# Extract unique types between two Tuples at compile time

**URL:** <https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476>\
**Category:** Performance\
**Tags:** tuple, generated\
**Created:** [February 5, 2026, 2:45pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476 "2026-02-05T14:45:00Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![albertomercurio](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/albertomercurio/32/27051_2.png) [@albertomercurio](https://discourse.julialang.org/u/albertomercurio)\
**Post date:** [February 5, 2026, 2:45pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/1 "2026-02-05T14:45:01Z")

</div>

Hello,

I have two Tuples `x` and `y` containing objects of different types. I now want to create at compile time a list containing only the unique types. I have tried this

```julia-auto
function test2(x::T1, y::T2) where {T1 <: Tuple, T2 <: Tuple}
    all_types = (T1.parameters..., T2.parameters...)
    return unique(all_types)
end

x = (1, 2, 3 + 1im)
y = (4, 5.0)

test2(x, y)

```

which however returns a `Vector`, so the length is not known, as well as the list of the types.

Is there a way to do it without using the `@generated` macro?

For example, if I have two `NamedTuple`s, the `merge` function does something similar to the `keys`, as they need to be merged.

---

<div class="post-metadata">

**Author:** ![StevenWhitaker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevenwhitaker/32/9749_2.png) [@StevenWhitaker](https://discourse.julialang.org/u/StevenWhitaker)\
**Post date:** [February 5, 2026, 3:37pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/2 "2026-02-05T15:37:00Z")

</div>

This was a fun exercise. I took inspiration from your comment about merging `NamedTuple`s and came up with the following. Not sure if it handles every case, but it works for the `x` and `y` you gave.

```julia
@inline function symbolize(T)
    if isempty(T.parameters)
        return T.name.name
    else
        params = ntuple(i -> symbolize(T.parameters[i]), length(T.parameters))
        return Symbol(T.name.name, "{", params..., "}")
    end
end

@inline function make_nt(T)
    name = (symbolize(T),)
    NamedTuple{name}(name)
end

function get_unique_types(x::T1, y::T2) where {T1 <: Tuple, T2 <: Tuple}
    nts1 = ntuple(i -> make_nt(T1.parameters[i]), length(T1.parameters))
    nts2 = ntuple(i -> make_nt(T2.parameters[i]), length(T2.parameters))
    ntc = merge(nts1..., nts2...)
    return keys(ntc)
end

```

> **\`@code\_warntype\` output**
>
> ```julia-repl
> julia> x = (1, 2, 3 + 1im); y = (4, 5.0);
> 
> julia> get_unique_types(x, y)
> (:Int64, Symbol("Complex{Int64}"), :Float64)
> 
> julia> @code_warntype get_unique_types(x, y)
> MethodInstance for get_unique_types(::Tuple{Int64, Int64, Complex{Int64}}, ::Tuple{Int64, Float64})
> from get_unique_types(x::T1, y::T2) where {T1<:Tuple, T2<:Tuple} @ Main REPL[1]:15
> Static Parameters
> T1 = Tuple{Int64, Int64, Complex{Int64}}
> T2 = Tuple{Int64, Float64}
> Arguments
> #self#::Core.Const(Main.get_unique_types)
> x::Tuple{Int64, Int64, Complex{Int64}}
> y::Tuple{Int64, Float64}
> Locals
> #3::var"#get_unique_types##2#get_unique_types##3"{Tuple{Int64, Float64}}
> #2::var"#get_unique_types##0#get_unique_types##1"{Tuple{Int64, Int64, Complex{Int64}}}
> ntc::@NamedTuple{Int64::Symbol, var"Complex{Int64}"::Symbol, Float64::Symbol}
> nts2::Tuple{@NamedTuple{Int64::Symbol}, @NamedTuple{Float64::Symbol}}
> nts1::Tuple{@NamedTuple{Int64::Symbol}, @NamedTuple{Int64::Symbol}, @NamedTuple{var"Complex{Int64}"::Symbol}}
> Body::Tuple{Symbol, Symbol, Symbol}
> 1 ─ %1 = Main.ntuple::Core.Const(ntuple)
> │ %2 = Main.:(var"#get_unique_types##0#get_unique_types##1")::Core.Const(var"#get_unique_types##0#get_unique_types##1")
> │ %3 = $(Expr(:static_parameter, 1))::Core.Const(Tuple{Int64, Int64, Complex{Int64}})
> │ %4 = Core.apply_type(%2, %3)::Core.Const(var"#get_unique_types##0#get_unique_types##1"{Tuple{Int64, Int64, Complex{Int64}}})
> │ (#2 = %new(%4))
> │ %6 = #2::Core.Const(var"#get_unique_types##0#get_unique_types##1"{Tuple{Int64, Int64, Complex{Int64}}}())
> │ %7 = Main.length::Core.Const(length)
> │ %8 = $(Expr(:static_parameter, 1))::Core.Const(Tuple{Int64, Int64, Complex{Int64}})
> │ %9 = Base.getproperty(%8, :parameters)::Core.Const(svec(Int64, Int64, Complex{Int64}))
> │ %10 = (%7)(%9)::Core.Const(3)
> │ (nts1 = (%1)(%6, %10))
> │ %12 = Main.ntuple::Core.Const(ntuple)
> │ %13 = Main.:(var"#get_unique_types##2#get_unique_types##3")::Core.Const(var"#get_unique_types##2#get_unique_types##3")
> │ %14 = $(Expr(:static_parameter, 2))::Core.Const(Tuple{Int64, Float64})
> │ %15 = Core.apply_type(%13, %14)::Core.Const(var"#get_unique_types##2#get_unique_types##3"{Tuple{Int64, Float64}})
> │ (#3 = %new(%15))
> │ %17 = #3::Core.Const(var"#get_unique_types##2#get_unique_types##3"{Tuple{Int64, Float64}}())
> │ %18 = Main.length::Core.Const(length)
> │ %19 = $(Expr(:static_parameter, 2))::Core.Const(Tuple{Int64, Float64})
> │ %20 = Base.getproperty(%19, :parameters)::Core.Const(svec(Int64, Float64))
> │ %21 = (%18)(%20)::Core.Const(2)
> │ (nts2 = (%12)(%17, %21))
> │ %23 = Main.merge::Core.Const(merge)
> │ %24 = nts1::Core.Const(((Int64 = :Int64,), (Int64 = :Int64,), (var"Complex{Int64}" = Symbol("Complex{Int64}"),)))
> │ %25 = nts2::Core.Const(((Int64 = :Int64,), (Float64 = :Float64,)))
> │ (ntc = Core._apply_iterate(Base.iterate, %23, %24, %25))
> │ %27 = Main.keys::Core.Const(keys)
> │ %28 = ntc::Core.Const((Int64 = :Int64, var"Complex{Int64}" = Symbol("Complex{Int64}"), Float64 = :Float64))
> │ %29 = (%27)(%28)::Core.Const((:Int64, Symbol("Complex{Int64}"), :Float64))
> └── return %29
> 
> ```

* * *

EDIT: This code outputs `Symbol`s, not types as requested in the OP. See [my later comment](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/4) for slight changes to return types instead.

---

<div class="post-metadata">

**Author:** ![albertomercurio](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/albertomercurio/32/27051_2.png) [@albertomercurio](https://discourse.julialang.org/u/albertomercurio)\
**Post date:** [February 5, 2026, 4:05pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/3 "2026-02-05T16:05:51Z")

</div>

Thanks! It works.

---

<div class="post-metadata">

**Author:** ![StevenWhitaker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevenwhitaker/32/9749_2.png) [@StevenWhitaker](https://discourse.julialang.org/u/StevenWhitaker)\
**Post date:** [February 5, 2026, 5:12pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/4 "2026-02-05T17:12:26Z")

</div>

I’m glad it works! Though I just realized that the function I gave you gives you `Symbol`s, not the types themselves. If you want the types themselves, define `make_nt` as:

```julia
@inline make_nt(T) = NamedTuple{(symbolize(T),)}((T,))

```

And then in `get_unique_types`:

```julia
return values(ntc)

```

---

<div class="post-metadata">

**Author:** ![longemen3000](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/longemen3000/32/7298_2.png) [@longemen3000](https://discourse.julialang.org/u/longemen3000)\
**Post date:** [February 5, 2026, 9:25pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/5 "2026-02-05T21:25:15Z")

</div>

for acessing the types of a tuple (or any other struct), the way to do it without using the fields of `DataType` is by using the function `fieldtypes`

```julia-auto
julia> m = (1.0,2,"a")
(1.0, 2, "a")

julia> fieldtypes(typeof(m))
(Float64, Int64, String)

```

---

<div class="post-metadata">

**Author:** ![StevenWhitaker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevenwhitaker/32/9749_2.png) [@StevenWhitaker](https://discourse.julialang.org/u/StevenWhitaker)\
**Post date:** [February 6, 2026, 10:27pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/6 "2026-02-06T22:27:53Z")

</div>

Thanks for the reminder. But using `fieldtypes`, and using `Symbol` instead of my custom `symbolize` (to avoid using the fields of `DataType`), seems to break the “at compile time” requirement of the OP. But maybe there’s another way to make it work at compile time without relying on the fields of `DataType`.

---

<div class="post-metadata">

**Author:** ![jules](https://avatars.discourse-cdn.com/v4/letter/j/41988e/32.png) [@jules](https://discourse.julialang.org/u/jules)\
**Post date:** [February 7, 2026, 10:30pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/7 "2026-02-07T22:30:41Z")

</div>

This function with tuple recursion seems to compile to consts

```julia
function _add_el(tup, new)
    new in tup ? tup : (tup..., new)
end

function _add(tup::Tuple, tup2::Tuple)
    if length(tup2) > 1
        x, rest... = tup2
        _add(_add_el(tup, x), rest)
    elseif length(tup2) == 1
        _add_el(tup, only(tup2))
    else
        tup
    end
end

_add((Int, Float64), String)

_add((Int, Float64), (String, Int))

function f(tup1, tup2)
    types1 = map(typeof, tup1)
    types2 = map(typeof, tup2)
    init = ()
    return _add(_add(init, types1), types2)
end

x = (1, 2, 3 + 1im)
y = (4, 5.0)

f(x, y)

```

---

<div class="post-metadata">

**Author:** ![aryavorskiy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/aryavorskiy/32/43466_2.png) [@aryavorskiy](https://discourse.julialang.org/u/aryavorskiy)\
**Post date:** [February 8, 2026, 12:39pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/8 "2026-02-08T12:39:51Z")

</div>

Looks like a perfect use case for a [generated function](https://docs.julialang.org/en/v1/manual/metaprogramming/#Generated-functions)

```julia
@generated function unique_types(a...)
    ret = Tuple(unique(a))
    return quote $ret end
end
f(tup1::Tuple, tup2::Tuple) = unique_types(tup1..., tup2...)

x = (1, 2, 3 + 1im)
y = (4, 5.0)
f(x, y)

```

---

<div class="post-metadata">

**Author:** ![StevenWhitaker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevenwhitaker/32/9749_2.png) [@StevenWhitaker](https://discourse.julialang.org/u/StevenWhitaker)\
**Post date:** [February 9, 2026, 3:42pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/9 "2026-02-09T15:42:37Z")

</div>

I agree, using `@generated` would have been my first thought for solving this, but the OP specifically asked

> Is there a way to do it without using the `@generated` macro?

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [February 9, 2026, 3:57pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/10 "2026-02-09T15:57:01Z")

</div>

If you don’t want to use `@generated` this is potentially a good use-case for `@assume_effects:`

```julia-auto
Base.@assume_effects :foldable _funique(args::Type...) = Tuple(unique(args))
unique_types(args...) = _funique(typeof.(args)...)
f(tup1, tup2) = unique_types(tup1..., tup2...)

```

This basically tells the compiler that `_funique` is allowed to be evaluated at compile time even though the compiler would normally not allow itself to do that due to the `Array` allocated by `unique`.

---

<div class="post-metadata">

**Author:** ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)\
**Post date:** [February 9, 2026, 7:20pm UTC](https://discourse.julialang.org/t/extract-unique-types-between-two-tuples-at-compile-time/135476/11 "2026-02-09T19:20:56Z")

</div>

You can get something similar to Mason’s answer without the need to `@assume_effects` if you’re ok getting a `Union` type back:

```julia-auto
julia> unique_types(args...) = _unique_types(Union{}, args...)
unique_types (generic function with 1 method)

julia> _unique_types(u) = u
_unique_types (generic function with 1 method)

julia> _unique_types(u, t) = Union{u, typeof(t)}
_unique_types (generic function with 2 methods)

julia> _unique_types(u, t, args...) = _unique_types(Union{u, typeof(t)}, args...)
_unique_types (generic function with 3 methods)

julia> unique_types(1, "2", 3.)
Union{Float64, Int64, String}

julia> @code_warntype unique_types(1, "2", 3.)
MethodInstance for unique_types(::Int64, ::String, ::Float64)
  from unique_types(args...) @ Main REPL[1]:1
Arguments
  #self#::Core.Const(Main.unique_types)
  args::Tuple{Int64, String, Float64}
Body::Type{Union{Float64, Int64, String}}
1 ─ %1 = Main._unique_types::Core.Const(Main._unique_types)
│ %2 = Main.Union::Core.Const(Union)
│ %3 = Core.apply_type(%2)::Core.Const(Union{})
│ %4 = Core.tuple(%3)::Core.Const((Union{},))
│ %5 = Core._apply_iterate(Base.iterate, %1, %4, args)::Core.Const(Union{Float64, Int64, String})
└── return %5

```

Tuples are a good example case where recursive splatting can be ok (as long as you keep splatting fewer and fewer elements).
