First let's augment your Constructor
definition so that it is generic in the instance type:
type Constructor<T = {}> = new (...args: any[]) => T;
That will make it easier to write the types in what follows.
My inclination here would be to define mix()
's call signature like this:
declare function mix<S extends object, M extends any[]>(
superclass: Constructor<S>,
mixins: [...{ [I in keyof M]: (c: Constructor<S>) => Constructor<M[I]> }
]
): Constructor<S & IntersectTuple<M>>
Let's walk through that. The function is generic in the type parameter S
corresponding to the instance type of the superclass
argument. So if superclass
is of type Constructor<Super>
, then S
is just Super
.
It's also generic in the type parameter M
corresponding to the tuple of instance types of the classes returned by the elements of the the mixins
argument. So if mixins
is [FooMixin, BarMixin, BazMixin]
, which return Constructor<Foo>
, Constructor<Bar>
and Constructor<Baz>
, respectively, then M
would be just [Foo, Bar, Baz]
.
The type of mixins
is a mapped tuple type over the numberlike indices I
of M
, where the I
th element of mixins
is of type (c: Constructor<S>) => Constructor<M[I]>
, as it accepts superclass
as an input, and produce a constructor of the I
th element of M
(of indexed access type M[I]
).
By the way, that mapped type is wrapped in [...
/]
and is thus a variadic tuple type. That does very little to the type of mixins
, but it gives the compiler a hint that we'd like it to infer a tuple type for M
and not an unordered array type. This is somewhat important given the return type of mix()
.
That return type is Constructor<S & IntersectTuple<M>>
, where IntersectTuple<M>
is a utility type that evaluates to the intersection of all the elements of M
. So if S
is Super
and M
is [Foo, Bar, Baz]
, then the return type will be Constructor<Super & Foo & Bar & Baz>
. That intersection means that the return type will construct instances of all those types at once. So it will construct things that are Super
and Foo
and Bar
and Baz
.
We need to define IntersectTuple
; here's one way to do it:
type IntersectTuple<T extends any[]> =
{ [I in keyof T]: (x: T[I]) => void }[number] extends
(x: infer R) => void ? R : never;
It works via the rules for conditional type inference which say that if multiple inference candidates appear in a contravariant position (see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript ) then the resulting inferred type will be the intersection of all those candidates. (Also see Transform union type to intersection type for more about this general technique.)
Okay, let's test it!
const MixedClass2 = mix(BaseNode, [PositionedNodeMixin, NodeChildrenMixin]);
// const MixedClass2: Constructor<
// BaseNode &
// PositionedNodeMixin<Constructor<{}>>.NodePositionableAbilities &
// NodeChildrenMixin<Constructor<{}>>.NodeChildrenAbilities
// >
const mixedClassInstance2 = new MixedClass2();
// const mixedClassInstance2:
// BaseNode &
// PositionedNodeMixin<Constructor<{}>>.NodePositionableAbilities &
// NodeChildrenMixin<Constructor<{}>>.NodeChildrenAbilities
mixedClassInstance2.id; // (property) BaseNode.id: string
mixedClassInstance2.children // (property) NodeChildrenAbilities.children:
// Record<string, boolean | undefined>
mixedClassInstance2.pos // (property) NodePositionableAbilities.pos: string
Looks good. The type of MixedClass2
is a constructor of the intersection of BaseNode
and NodePositionAbilities
and NodeChildrenAbilities
, and so mixedClassInstance2
has all the expected properties coming from the expected places.
Playground link to code