Skip to content

Generics inference of type (static) intersection on instance shape #7934

Closed
@shlomiassaf

Description

@shlomiassaf

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 be ConcreteTypeOf<{}>, 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions