Description
I'v build a nice library for composition of types (mixin) in a dynamic way so the bookkeeping overhead of having to write stand-in members is not needed. (the lib also has some other features and customisation for composition).
It works great but I have one last bookkeeping overhead that bothers me, The lib requires to explicitly set the Type parameters since I can't get it to work when I let typescript infer the types.
Here's an example of what i'm facing:
TypeScript Version:
1.8.9
Code
A
and B
are "example" classes, we will use them for demonstration.
Each have one static
member and one instance
member, we will use them verify the output when mixin them together.
class A {
static STATIC_A: string = 'A';
instanceA: string = 'A';
constructor() { }
}
class B {
static STATIC_B: string = 'B';
instanceB: string = 'B';
}
A "naive" approach using a simple function:
function mergeTypes<T, Z>(type1: T, type2: Z): T & Z {
// Do some work to mixin the types.
return <any>{};
}
let AB = mergeTypes(A, B); // Inferred AB: typeof A & typeof B
let ab = new AB(); // Inferred ab: A
AB.STATIC_A; // Compiler OK
AB.STATIC_B; // Compiler OK
ab.instanceA; // Compiler OK
ab.instanceB; // Compiler ERR: Property 'instanceB' does not exist on type 'A'
Conclusion: Intersection of static types will not propagate to the instance shape.
A more explicit approach with a control unit and state.
// Define a contract to bind an instance type param (T/Z) to its static "parent" (TType/ZType)
interface ConcreteTypeOf<T> extends Function { new (...args): T; }
// Explicitly say what we are dealing with:
class MergeTypes<T, TType extends ConcreteTypeOf<T>, Z, ZType extends ConcreteTypeOf<Z>> {
constructor(private type1: TType, private type2: ZType) {}
// We return a constructor for T & Z and intersect static members.
merge(): ConcreteTypeOf<T & Z> & TType & ZType {
// Do some work to mixin the types.
return <any>{};
}
}
An implicit attempt first, let the compiler infer for us
let AB = new MergeTypes(A, B).merge(); // Inferred AB: ConcreteTypeOf<{}> & typeof A & typeof B
let ab = new AB(); // Inferred ab: {}
AB.STATIC_A; // Compiler OK
AB.STATIC_B; // Compiler OK
ab.instanceA; // Compiler ERR: Property 'instanceA' does not exist on type '{}'
ab.instanceB; // Compiler ERR: Property 'instanceB' does not exist on type '{}'
It turns out that the compiler infers
ConcreteTypeOf<T & Z>
to beConcreteTypeOf<{}>
, WHY?
Finally, a working version, but we need to explicitly express what we want:
let AB = new MergeTypes<A, typeof A, B, typeof B>(A, B).merge(); // Inferred AB: ConcreteTypeOf<A & B> & typeof A & typeof B
let ab = new AB(); // Inferred ab: A & B
AB.STATIC_A; // Compiler OK
AB.STATIC_B; // Compiler OK
ab.instanceA; // Compiler OK
ab.instanceB; // Compiler OK
If i'll be able to remove the need to explicitly express the type I will be able to have an easy API for composition/mixin what ever, that can create type's on the fly and save a reference for them (compile time type reference) as if they were defined expressively.