There’s no way to make it so that simply writing P{A, A} errors, if that’s what you want. If you call dump(P), you get
julia> dump(P)
UnionAll
var: TypeVar
name: Symbol F
lb: Core.TypeofBottom Union{}
ub: Union{A, B, C, D}
body: UnionAll
var: TypeVar
name: Symbol S
lb: Core.TypeofBottom Union{}
ub: Union{A, B, C, D}
body: P{F<:Union{A, B, C, D},S<:Union{A, B, C, D}} <: Any
a::Array{T,1} where T
b::Array{T,1} where T
This shows how UnionAlls like P are represented in Julia. You can have upper bounds (ub) and lower bounds (lb) on type variables, but UnionAll itself does not support more complicated constraints, probably to limit the computational complexity of type system queries (but I’m far from an expert on this subject).