# \`Tuple\` of \`Union\` or \`Union\` of \`Tuple\`?

**URL:** <https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311>\
**Category:** General Usage\
**Tags:** tuple, type-stability, union\
**Created:** [December 2, 2025, 9:05pm UTC](https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311 "2025-12-02T21:05:54Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![frankwswang](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/frankwswang/32/18561_2.png) [@frankwswang](https://discourse.julialang.org/u/frankwswang)\
**Post date:** [December 2, 2025, 9:05pm UTC](https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311/1 "2025-12-02T21:05:54Z")

</div>

I want to annotate a type-unstable variable with a type-parameter constraint to improve the chance of union splitting during compilation. In general, should I do `Tuple` of `Union` or `Union` of `Tuple`?

For example, which of the following `struct` is better, assuming `T1` and `T2` have different underlying data structures?

```julia-auto
struct TofU{T1, T2}
    a::NTuple{2, Union{T1, T2}}
end

struct UofT{T1, T2}
    a::Union{Tuple{T1, T2}, Tuple{T2, T1}, NTuple{2, T1}, NTuple{2, T2}}
end

```

---

<div class="post-metadata">

**Author:** ![frankwswang](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/frankwswang/32/18561_2.png) [@frankwswang](https://discourse.julialang.org/u/frankwswang)\
**Post date:** [December 2, 2025, 9:13pm UTC](https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311/2 "2025-12-02T21:13:05Z")

</div>

Note that for any given `T1` and `T2` (`T1 != T2`), though `NTuple{2, Union{T1, T2}}` and `Union{Tuple{T1, T2}, Tuple{T2, T1}, NTuple{2, T1}, NTuple{2, T2}}` represent two “equal” types, they are not identical (egal):

```julia-auto
julia> T1 = Int
Int64

julia> T2 = Float64
Float64

julia> t1 = NTuple{2, Union{T1, T2}}
Tuple{Union{Float64, Int64}, Union{Float64, Int64}}

julia> t2 = Union{Tuple{T1, T2}, Tuple{T2, T1}, NTuple{2, T1}, NTuple{2, T2}}
Union{Tuple{Float64, Float64}, Tuple{Float64, Int64}, Tuple{Int64, Float64}, Tuple{Int64, Int64}}

julia> t1 == t2
true

julia> t1 === t2
false

```

Hence, I assume there is some non-negligible overhead difference in compilation or dispatch process.

---

<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:** [December 2, 2025, 9:15pm UTC](https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311/3 "2025-12-02T21:15:06Z")

</div>

Only unions of tuples participate in the union-splitting optimizations, so those tend to perform better.

---

<div class="post-metadata">

**Author:** ![jakobnissen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jakobnissen/32/13477_2.png) [@jakobnissen](https://discourse.julialang.org/u/jakobnissen)\
**Post date:** [December 3, 2025, 8:57am UTC](https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311/4 "2025-12-03T08:57:47Z")

</div>

For some silly reason, tuples in Julia are covariant. Hence, `Tuple{Union{A, B}}` is a UnionAll type. I think this is basically a straight up design flaw in Julia (see [Invariant Tuples · Issue #24614 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/issues/24614)), which stems from the inadvisable idea of having function parameters (which must be covariant) be the same thing as tuples.

So use unions of tuples? Well, those suffer from combinatorial explosions. If your tuple have 6 fields each with 3 possibilities, the resulting union contains 3^6 = 729 concrete tuple types. In this case, you should use named tuples, which, unlike ordinary tuples, are invariant.

So:

- Use named tuples of unions if individual fields of your union are union-typed
- Use unions of tuples if you really have distinct tuple types, and not the same tuple type with union-typed fields

---

<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:** [December 3, 2025, 11:35am UTC](https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311/5 "2025-12-03T11:35:23Z")

</div>

> [@jakobnissen](#):
>
> I think this is basically a straight up design flaw in Julia (see [Invariant Tuples · Issue #24614 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/issues/24614)), which stems from the inadvisable idea of having function parameters (which must be covariant) be the same thing as tuples.

Not to mention that despite making this weird choice, function parameters are **not** the same as `Tuple`s in at least a few observable ways! e.g. function parameters can specialize on `Type{<:T}`, whereas `Tuple` only specializes to `DataType`:

```julia-auto
julia> f(::Type{T}) where {T} = T;

julia> f(Int)
Int64

julia> typeof((Int,))
Tuple{DataType}

julia> let args = (Int,)
           @btime f($args...)
       end
  157.771 ns (1 allocation: 32 bytes)
Int64

```

---

<div class="post-metadata">

**Author:** ![frankwswang](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/frankwswang/32/18561_2.png) [@frankwswang](https://discourse.julialang.org/u/frankwswang)\
**Post date:** [December 3, 2025, 9:18pm UTC](https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311/6 "2025-12-03T21:18:15Z")

</div>

> [@jakobnissen](#):
>
> For some silly reason, tuples in Julia are covariant. Hence, `Tuple{Union{A, B}}` is a UnionAll type.

For a given pair of `(A::DataType, B::DataType)`, I think `Tuple{Union{A, B}}` is just a `Tuple` rather than a `UnionAll`.

> [@jakobnissen](#):
>
> I think this is basically a straight up design flaw in Julia (see [Invariant Tuples · Issue #24614 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/issues/24614)), which stems from the inadvisable idea of having function parameters (which must be covariant) be the same thing as tuples.

Personally, I’m fine with having `Tuple` being covariant by construction, but it would be nice if `Tuple` of `Union` can be automatically transpiled into `Union` of `Tuple` for better type inference and optimization. But then again, you have pointed out the risk/footgun of such an exponentially expressive type construction enabled by `Tuple` and `Union`:

> [@jakobnissen](#):
>
> So use unions of tuples? Well, those suffer from combinatorial explosions. If your tuple have 6 fields each with 3 possibilities, the resulting union contains 3^6 = 729 concrete tuple types.

---

<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:** [December 3, 2025, 9:38pm UTC](https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311/7 "2025-12-03T21:38:51Z")

</div>

I’m struggling to see how the covariance or combinatorial explosions are relevant here. The small union optimizations only handle small unions — N=4 or less — which is just fitting the use-case here. It’s not going to be advantageous to do a big union anyhow.

Doing better for tuples of small unions (which, yes, would necessarily need to net to small combinatorics) is:

> <https://github.com/JuliaLang/julia/issues/57054>
>
> For example, these two functions look like they should be equivalent, but the in…ferred return type of the first is a small union, as desired, while the other one isn't. Using (parametric) structs instead of tuples gives similar results.
> \`\`\`julia
> function bar(x, n)
> return n \< 0 ? (inv(x), inv(n)) : (x, n)
> end
> 
> function bar2(x, n)
> y, k = n \< 0 ? (inv(x), inv(n)) : (x, n)
> return (y, k)
> end
> \`\`\`
> With the return type of \`bar2\` in red, \`bar\` in yellow
> \`\`\`julia
> julia\> @code\_warntype bar(1, -2)
> MethodInstance for bar(::Int64, ::Int64)
> from bar(x, n) @ Main REPL\[1\]:1
> Arguments
> #self#::Core.Const(Main.bar)
> x::Int64
> n::Int64
> Body::Union{Tuple{Float64, Float64}, Tuple{Int64, Int64}}
> 1 ─ %1 = Main.:\<::Core.Const(\<)
> │ %2 = (%1)(n, 0)::Bool
> └── goto #3 if not %2
> 2 ─ %4 = Main.inv(x)::Float64
> │ %5 = Main.inv(n)::Float64
> │ %6 = Core.tuple(%4, %5)::Tuple{Float64, Float64}
> └── return %6
> 3 ─ %8 = Core.tuple(x, n)::Tuple{Int64, Int64}
> └── return %8
> 
> julia\> @code\_warntype bar2(1, -2)
> MethodInstance for bar2(::Int64, ::Int64)
> from bar2(x, n) @ Main REPL\[3\]:1
> Arguments
> #self#::Core.Const(Main.bar2)
> x::Int64
> n::Int64
> Locals
> @\_4::Int64
> k::Union{Float64, Int64}
> y::Union{Float64, Int64}
> @\_7::Union{Tuple{Float64, Float64}, Tuple{Int64, Int64}}
> Body::Tuple{Union{Float64, Int64}, Union{Float64, Int64}}
> 1 ─ Core.NewvarNode(:(@\_4))
> │ Core.NewvarNode(:(k))
> │ Core.NewvarNode(:(y))
> │ %4 = (n \< 0)::Bool
> └── goto #3 if not %4
> 2 ─ %6 = Main.inv(x)::Float64
> │ %7 = Main.inv(n)::Float64
> │ (@\_7 = Core.tuple(%6, %7))
> └── goto #4
> 3 ─ (@\_7 = Core.tuple(x, n))
> 4 ┄ %11 = @\_7::Union{Tuple{Float64, Float64}, Tuple{Int64, Int64}}
> │ %12 = Base.indexed\_iterate(%11, 1)::Union{Tuple{Float64, Int64}, Tuple{Int64, Int64}}
> │ (y = Core.getfield(%12, 1))
> │ (@\_4 = Core.getfield(%12, 2))
> │ %15 = @\_4::Int64
> │ %16 = Base.indexed\_iterate(%11, 2, %15)::Union{Tuple{Float64, Int64}, Tuple{Int64, Int64}}
> │ (k = Core.getfield(%16, 1))
> │ %18 = y::Union{Float64, Int64}
> │ %19 = k::Union{Float64, Int64}
> │ %20 = Core.tuple(%18, %19)::Tuple{Union{Float64, Int64}, Union{Float64, Int64}}
> └── return %20
> \`\`\`

---

<div class="post-metadata">

**Author:** ![frankwswang](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/frankwswang/32/18561_2.png) [@frankwswang](https://discourse.julialang.org/u/frankwswang)\
**Post date:** [December 3, 2025, 10:45pm UTC](https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311/8 "2025-12-03T22:45:38Z")

</div>

I agree that the discussion of “combinatorial explosions” is not directly related to the OP if we focus only on small unions.

If we do want to extend the discussion, I have a slightly more relevant question. Say we do have a `Tuple` of `Union` that, when rewritten into `Union` of `Tuple`, will result in a `Union` of more than four concrete types–meaning we can no longer utilize the benefit of union splitting (by default). Is it possible to directly optimize the code stability with a `Tuple` of `Union`?

---

<div class="post-metadata">

**Author:** ![jakobnissen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jakobnissen/32/13477_2.png) [@jakobnissen](https://discourse.julialang.org/u/jakobnissen)\
**Post date:** [December 4, 2025, 10:28am UTC](https://discourse.julialang.org/t/tuple-of-union-or-union-of-tuple/134311/9 "2025-12-04T10:28:12Z")

</div>

> [@frankwswang](#):
>
> For a given pair of `(A::DataType, B::DataType)`, I think `Tuple{Union{A, B}}` is just a `Tuple` rather than a `UnionAll`.

It’s not a UnionAll in the sense that it’s an instance of `UnionAll`, but it behaves exactly like one (i.e. it has an equivalent UnionAll, as it refers to the same set of types) :

```julia
julia> UA = Tuple{T} where T <: Union{Int, UInt};

julia> UA isa UnionAll
true

julia> UA == Tuple{Union{Int, UInt}}
true

```

As you very well pointed out, tuples of unions cannot be converted to unions of tuples without a combinatorial explosion. Note that this problem is unique for tuples, which is one reason the covariance is such a bad idea.

> [@mbauman](#):
>
> I’m struggling to see how the covariance or combinatorial explosions are relevant here.

It’s the covariance that causes tuples of unions to be non-concrete types, which is what causes the whole issue of OP in the first place. If they were invariant, like almost every other type in Julia\[1\] `NTuple{2, Union{T1, T2}}` would be concrete, and there would be no problem. [Issue 57054](https://github.com/JuliaLang/julia/issues/57054) would not be a thing.

You’re right that the OP example case happens to lie just on the threshold for (the internal compiler heuristic of) union splitting, but in the general case, which I what I assumed OP was interested in, one cannot rely on union splitting, as the number of possible types quickly exceeds the threshold.

* * *

1. Union types are, and need to be covariant, but I can’t think of any other type.
