Is there a way to restrict two fields in a struct to either be both `AbstractTypeA

Is there a way to restrict two fields in a struct to either be both AbstractTypeA or both AbstractTypeB, but not necessarily the same concrete type?

Note that the original poster on Slack cannot see your response here on Discourse. Consider transcribing the appropriate answer back to Slack, or pinging the poster here on Discourse so they can follow this thread.
(Original message :slack:) (More Info)

Seems like a good question.

I can’t see an easy way to do this. Here’s an initial attempt which is a bit convoluted but seems to work. Also not sure about the implementation of common_supertype function.

abstract type A end
abstract type B end
struct A1 <: A end
struct A2 <: A end
struct B1 <: B end
struct B2 <: B end

const Parents = Union{A,B}

function common_supertype(x::X, y::Y) where {X, Y}
    return first(intersect(supertypes(X), supertypes(Y)))

function common_supertype(x::X, y::Y) where {X, Y}
    supertypes_x = supertypes(X)
    i = findfirst(S -> y isa S, supertypes_x)
    return supertypes_x[i]

struct Bar{P<:Parents, X<:P, Y<:P}
    Bar{P,X,Y}(x::X, y::Y) where {P<:Parents, X<:P, Y<:P} = new{P,X,Y}(x, y)

function Bar(x::X, y::Y) where {X,Y}
    P = common_supertype(x, y)
    P <: Parents || throw(ArgumentError("Cannot construct Bar(x::$X, y::$Y) where arguments do not have a common supertype"))
    Bar{P,X,Y}(x, y)

a1, a2 = A1(), A2()
b1, b2 = B1(), B2()

Bar(a1, a2)
Bar(b1, b2)
Bar(a1, b2)

Two solutions have been mentioned on slack.

One by @giordano using an extra type parameter:

julia> struct Foo{T<:Union{Integer,AbstractFloat}, A<:T, B<:T}

julia> Foo{AbstractFloat,Float32,Float64}(1.0, 2.0)
Foo{AbstractFloat, Float32, Float64}(1.0f0, 2.0)

julia> Foo{Integer,UInt8,Int32}(1, 2)
Foo{Integer, UInt8, Int32}(0x01, 2)

julia> Foo{Integer,Float64,Int32}(1.0, 2)
ERROR: TypeError: in Foo, in A, expected A<:T<:Union{AbstractFloat, Integer}, got Type{Float64}
 [1] top-level scope
   @ REPL[4]:100:

One by @jakobnissen by ensuring type equality during construction:

struct Foo{A, B}
	function Foo{A, B}(x::A, y::B) where {A, B}
		if !((A <: Integer && B <: Integer) || (A <: AbstractFloat && B <: AbstractFloat))
			error("In Foo, Types A and B must have same supertype")
		new{A, B}(x, y)