This was a fun exercise. I took inspiration from your comment about merging NamedTuples and came up with the following. Not sure if it handles every case, but it works for the x and y you gave.
@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> 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 Symbols, not types as requested in the OP. See my later comment for slight changes to return types instead.