id(::Type{T}) where {T} = T
some_type{7,Int} == id(some_type){7,Int}

Naively, it seems like the comparison on the second line should return true for any type some_type, assuming neither type application throws. After all, id is the identity function, so the second line should be equivalent to the obvious tautology some_type{7,Int} == some_type{7,Int}, right?

Well, id is the identity in the ==-sense, but not in the ===-sense. And == between UnionAll types isn’t affected by type variable order. Thus:

julia> some_type = AbstractArray{T,N} where {N,T} # note the reversed type variable order
AbstractArray{T, N} where {N, T}
julia> id(::Type{T}) where {T} = T
id (generic function with 1 method)
julia> some_type{7,Int} == id(some_type){7,Int}
false
julia> some_type == id(some_type)
true
julia> some_type === id(some_type)
false
julia> AbstractArray === id(some_type)
true
julia> some_type === identity(some_type)
true

So id(some_unionall) effectively normalizes the type, and thus the unionall type variable order. I guess my conclusion here is to take care to do the normalization as soon as possible, to prevent shooting myself in the foot.

EDIT: a similar example:

julia> T = AbstractArray{T,N} where {N,T}
AbstractArray{T, N} where {N, T}
julia> f(::Type{S}) where {S<:T} = S === T
f (generic function with 1 method)
julia> f(T)
false

julia> some_type = AbstractArray{T,N} where {N,T}
AbstractArray{T, N} where {N, T}
julia> Type{some_type}
Type{AbstractArray}
julia> Vector{some_type} # any parametric type does this
Vector{AbstractArray} (alias for Array{AbstractArray, 1})
julia> id(::Type{some_type}) = some_type;
julia> methods(id)
# 1 method for generic function "id" from Main:
[1] id(::Type{AbstractArray})
@ REPL[11]:1
julia> id(AbstractArray) # well that's weird
AbstractArray{T, N} where {N, T}
julia> identity(AbstractArray) # phew, thanks to identity(@nospecialize x)
AbstractArray
julia> identity(some_type) # phew: the sequel
AbstractArray{T, N} where {N, T}

Maybe it’s worth clarifying in the type parameter docs that parameters are normalized for whatever reason, or whether that’s even a language semantic or a fringe type implementation detail.

EDIT: I’m pretty sure concrete types can’t be == and !==, but evidently not all such abstract types get normalized as parameters. Might be nice to know which and why.

Normalization also happens at method argument annotations, maybe because they are parameters of an underlying tuple for the method?

julia> foo(::some_type) = 0
foo (generic function with 1 method)
julia> methods(foo)
# 1 method for generic function "foo" from Main:
[1] foo(::AbstractArray)
@ REPL[4]:1