to create a container that can take Strings and Vector{String}s as its values.

The important difference between Foo{Union{Bar, Baz}} and Foo{<:Union{Bar,Baz}} is that the latter is the union over allFoo{T} where T is a subtype of Union{Bar, Baz} (so Foo{T} where T <: Union{Bar, Baz}. That construct is called a UnionAll and is very useful to write parametric code, but cannot be instantiated.

In the first half, Dict{String,<:Any} is equivalent syntax to Dict{String,T} where T<:Any; that is an iterated union a.k.a. UnionAll. Iterated unions are abstract types, so they can’t be instantiated; you can check with isconcretetype:

Now for the second half, this is a common mistake, so it’s documented halfway into this section. Note how Dict{String,Any} is a concrete type, and a concrete type cannot be a supertype of another concrete type. The reason it’s a common mistake is that people see the Any and think “isn’t that abstract?” Correct, a concrete parametric type can have abstract types as parameters! It’s commonly a way for Julia to make containers that contain multiple concrete types: Vector{Int} can only hold Int instances, while Vector{Integer} holds references to instances of any subtypes of Integer.

Extras:

It’s worth mentioning that a very important container is an exception to the “concrete parametric type with abstract type parameters” rule: Tuple (and NTuple and Vararg by extension, but not NamedTuple). So Tuple{Integer} is actually abstract and is a supertype of Tuple{Int}. It represents the same set of types as an iterated union Tuple{T} where T<:Integer, but note that it isn’t one. The reason for this special treatment is that Tuple is a Core type intended to package concrete types together in one spot, so there wasn’t much point in allowing abstract type parameters that require references to distant spots. There’s been discussions of possibly removing this special treatment in v2.

there’s a very niche case where a concrete type can be a supertype of an abstract type: Type{Int} <: DataType. It doesn’t have to be Int, it can be any type that is an instance of DataType.

struct Point{T}
x::T
y::T
end
function norm(p::Point{<:Int})
sqrt(p.x^2 + p.y^2)
end
norm(Point{Float64}(1.5,2.5))

results in

ERROR: LoadError: MethodError: no method matching norm(::Point{Float64})

for me. If you try

struct Point{T}
x::T
y::T
end
function norm(p::Point{<:Float64})
sqrt(p.x^2 + p.y^2)
end
function norm(p::Point{<:Int})
sqrt(p.x^2 + p.y^2)
end
methods(norm)

you’ll see something like

# 2 methods for generic function "norm":
[1] norm(p::Point{<:Float64}) in Main at ...
[2] norm(p::Point{<:Int64}) in Main at ...

It’s probably because you already defined Point for Real. Defining it for Int just adds a new method on top of the old. You can add new methods on top of each other as many times as you like, that is how you use multiple dispatch.

Restart Julia to clear the old method definitions.

BTW, instead of Point{<:Int}, just write Point{Int}. Int and Float64 have no subtypes, so <:Int is just the same as Int.