There’s a reflection function:
help?> isdispatchtuple
isdispatchtuple(T)
Determine whether type T is a tuple "leaf type", meaning it could appear as a type signature in dispatch and has no subtypes (or supertypes) which could appear in a call
julia> isdispatchtuple(Tuple{Int})
true
julia> isdispatchtuple(Tuple{Any})
false
The latter occurs when the call to your function is badly typed:
julia> foo(x) = x
julia> bar(x) = foo(first(x))
julia> code_warntype(bar, Tuple{Vector{Int}})
Body::Int64
1 1 ─ ...
│ %5 = invoke Main.foo(%4::Int64)::Int64
└── return %5
julia> code_warntype(bar, Tuple{Vector{Any}})
Body::Any
1 1 ─ ...
│ %5 = (Main.foo)(%4::Any)::Any
└── return %5
In that case, doing dynamic dispatch will compile a specialized version for the concrete run-time types which is typically faster (ie. the function barrier pattern).