Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
shlomiassaf opened this issue Apr 7, 2016 · 10 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@shlomiassaf
Copy link

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.

@shlomiassaf
Copy link
Author

BTW, I know that I'm sending the static type to the constructor so the compiler only "knows" about TType since T is not part of the constructor.

This is probably why T&Z = {}

I just don't know how to "extract" the instance shape from a static type in a generic form.
i.e: If I have class A{} I know that A is the instance shape and typeof A is the anonymous static type but when typeof A is a type parameter (T) the compiler doesn't have an explicit contract to infer instance shape from it's static type, and I don't know of any keyword that might do so in the language

@shlomiassaf
Copy link
Author

@RyanCavanaugh I'd love to hear your insights on this.

@DanielRosenwasser
Copy link
Member

It sounds like you potentially want to pass in the prototypes of each constructor, or you want to write

function mergeTypes<T, Z>(type1: new() => T, type2: new() => Z): new () => T & Z {
    // Do some work to mixin the types...
}

Also see some of the discussion that went on over at #4559.

@shlomiassaf
Copy link
Author

@DanielRosenwasser
In your example, T and Z are the instance type.
When you return new () => T&Z you actually return a "blank" type that only "knows" to creates an instance of the mixed in T&Z however all static metadata about this type is lost.

This is a complex issue, so I will try to simplify a bit, I will only address the core issue I see:

// Simple class that holds a Type.
// Has 2 type param's that are bound and represent both "types" of a class.
class TypeWrapper<T, TType extends new () => T> {
    constructor(private _type: TType) {}
    getType(): TType {
        return this._type;
    }
}

// A factory of TypeWrapper
// this trick enables return the typeof type dynamically 
function make<T, TType extends new () => T>(type: TType) {
    return new TypeWrapper<T, typeof type>(type);
}

let typeWrapper = make(class A {}); 
const Clazz = typeWrapper.getType();
let cls = new Clazz();

Now, what did the compiler inferred as type for Clazz?
image
typeof A, Looks legit :)

If we look at the instance cls it also looks great, the compiler inferred A for cls.

But what does the compiler think about timeWrapper?
image
TypeWrapper<{}, typeof A> notice the {} and not A.
Well, it knows about the TType param but nothing about T so it makes it {}.

The compiler knows that TType is a constructor for T but It only get TType in the signature.
Why can't it infer T from TType, or in other words, given typeof X can the compiler infer X?

It's seems strange that the compiler knows cls is A and Clazz is typeof A but inside TimeWrapper it doesn't know about T.

NOTE: In this situation Clazz and cls are good and we can continue as is, but when trying to manipulate the type, inside TypeWrapper is impossible since T is {}, any intersection is now invalid.

@Arnavion
Copy link
Contributor

Arnavion commented Apr 8, 2016

btw in the playground atleast, hovering over AB in new AB() from the "naive" code sample in the OP throws an error Cannot read property 'flags' of undefined. I didn't see if it's fixed in master.

image

editor.main.js:6 Uncaught Error: TypeError: Cannot read property 'flags' of undefined

TypeError: Cannot read property 'flags' of undefined
    at Object.t [as buildSymbolDisplay] (https://www.typescriptlang.org/play/Script/vs/languages/typescript/common/lib/typescriptServices.js:22:3918)
    at https://www.typescriptlang.org/play/Script/vs/languages/typescript/common/lib/typescriptServices.js:32:21522
    at p (https://www.typescriptlang.org/play/Script/vs/languages/typescript/common/lib/typescriptServices.js:32:21307)
    at Object.f [as symbolToDisplayParts] (https://www.typescriptlang.org/play/Script/vs/languages/typescript/common/lib/typescriptServices.js:32:21480)
    at H (https://www.typescriptlang.org/play/Script/vs/languages/typescript/common/lib/typescriptServices.js:35:11607)
    at Object.j [as getQuickInfoAtPosition] (https://www.typescriptlang.org/play/Script/vs/languages/typescript/common/lib/typescriptServices.js:35:14991)
    at Object.i [as compute] (https://www.typescriptlang.org/play/Script/vs/languages/typescript/common/typescriptWorker2.js:1:6652)
    at t.computeInfo (https://www.typescriptlang.org/play/Script/vs/languages/typescript/common/typescriptWorker2.js:2:26137)
    at https://www.typescriptlang.org/play/Script/vs/languages/typescript/common/typescriptMode.js:1:19518
    at then (https://www.typescriptlang.org/play/Script/vs/base/common/worker/workerServer.js:1:24735)(anonymous function) @ editor.main.js:6
editor.main.js:11 Main Thread sent to worker the following message:e._onError @ editor.main.js:11e._onmessage @ editor.main.js:11e._onSerializedMessage @ editor.main.js:11(anonymous function) @ editor.main.js:11worker.onmessage @ editor.main.js:14

@RyanCavanaugh
Copy link
Member

One solution would be #7234. That would make the "simplified" example work as expected. This is sort of an abuse of generics since you don't really want two type parameters there, but would work.

The alternative is #6606 (assuming it supported new), since then you would be able to write out the return type as { new(): (typeof new A()) & (typeof new B()) } & A & B }.

@mhegazy
Copy link
Contributor

mhegazy commented Apr 8, 2016

filed #7966 to track the crash in quick info

@shlomiassaf
Copy link
Author

@RyanCavanaugh #7234 and #6606 are spot on!

Here's the interesting thing about your suggestion with #6606:

As I said, I'm working on a lib for mixin's and type composition with realtime type composition.
So the core of it is in the following class, which is similar to my "simplified" exapmle:

export interface ConcreteTypeOf<T> extends Function { new (...args): T; }

class TypeWrapper<T, TType extends ConcreteTypeOf<T>> {

    constructor(private _type: new () => T, dupe: TType = undefined) {}

    get asType(): T {  return <any>this._type;  }
    get asStaticType(): TType { return <any>this._type; }

    mixType<Z, ZType extends new () => Z> (t: ConcreteTypeOf<Z>, dupe: ZType = undefined) 
                        : TypeWrapper<T & Z, ConcreteTypeOf<T & Z> & TType & ZType>  {
         return <any>this;
    }
}

Here are some notable changes from the "simplified" example:

  • Added a "dupe" param to the constructor, this is the workaround to get static type meta. if a user supply it he we will have "static" type meta, so new TypeWrapper(A, A) will have static & instance types of A and the compiler knows about T and TType.
  • Added to getter to get both instance type and static type of the dynamic class.
  • Add a mixType method, gets a type and returned the mixed type (no implementation), same logic as constructor regarding dupe type.

Note the return type TypeWrapper<T & Z, ConcreteTypeOf<T & Z> & TType & ZType> same as your suggestion with #6066 but with a redundant parameter.

Here's an example of what we get from it:

export class A{
    static IM_STATIC_A: string;
    imInstanceA: string;
}

class B_{
    static IM_STATIC_B: string;
    imInstanceB: string;
}
class C_{
    static IM_STATIC_C: string;
    imInstanceC: string;
}
let tw = new TypeWrapper(A, A)
    .mixType(B_, B_)
    .mixType(C_, C_);

// let's expose out our new type to the world:
export const Composed = tw.asStaticType; 
export type Composed = typeof tw.asType;

image
image

Quite Cool!

However I had to remove the intersection of TType & ZType from the return type of mixinType in TypeWrapper which means the static type is gone.

    mixType<Z, ZType extends new () => Z> (t: ConcreteTypeOf<Z>, dupe: ZType = undefined) 
                        : TypeWrapper<T & Z, ConcreteTypeOf<T & Z>>  {
         return <any>this;
    }

Here's why...
Lets mixin with, the output of a previous mixin, Composed as base:

class D {
    static IM_STATIC_D: string;
    imInstanceD: string;
}

let twD = new TypeWrapper(Composed,Composed)
    .mixType(D, D);

export const DComposed = twD.asStaticType; 
export type DComposed = typeof twD.asType;

If we don't remove the TType & ZType intersection from the return type of mixType:
image

Note the error and the instance shape is only D now.

If we remove it, we loose static type meta but have full instance shape.
image

Also worth noting, both with TType & ZType and without it, the new type is not "extendable"
With it:
image

Without it:
image

I really like the outcome, even without static meta tough its an issue.
Beside's mixing in 2 types I manages to mixing single members which make composition really cool.

If these issues are solved it will make the type system super super flexible.

Thanks!

@shlomiassaf
Copy link
Author

@RyanCavanaugh
After playing with typeof expressions using the compiler built for #6066 I'm quite positive now that
#7234 will actually solve the problem.

Now matter what you do, if the compiler cant infer an instance type from it's static type you can't fill the missing gap.

@mhegazy
Copy link
Contributor

mhegazy commented Apr 11, 2016

Closing in favor of #7234

@mhegazy mhegazy closed this as completed Apr 11, 2016
@mhegazy mhegazy added the Duplicate An existing issue was already created label Apr 11, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants