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

Suggestion: use receiver's declared type arguments as defaults to its class constructor #7782

Open
malibuzios opened this issue Apr 1, 2016 · 4 comments
Labels
Effort: Difficult Good luck. Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@malibuzios
Copy link

I run into this relatively simple pattern a lot:

class GenericClass<T, U, V> {
    a: T;
    b: U;
    c: V;
}

let x: GenericClass<number, string, boolean>;

x = new GenericClass();
// Which produces the error:
// Error: GenericClass<{}, {}, {}> is not assignable to GenericClass<number, string, boolean>

Since x has already been defined with a particular set of type arguments, these arguments could be implicitly assumed as defaults to its type's constructor when invoked with it being the receiver (for simplicity I'm assuming here it is the exact constructor as declared e.g. not a derived class).

I understand there will be cases where that would be very difficult or even impossible to achieve:

let createInstance = () => new GenericClass()
x = createInstance();

but these are relatively rare. Most of the time it's just:

class Example {
    myMap: Map<string, Array<{n: number}>>;

    constructor() {
        this.myMap = new Map<string, Array<{n: number}>>(); // <-- Why repeat?
    }
}

Which instead could have been:

class Example {
    myMap: Map<string, Array<{n: number}>>;

    constructor() {
        this.myMap = new Map(); // <-- Better.. :)
    }
}
  • Any possible issues with this? perhaps ones that I'm not aware of or haven't thought of?
  • Potential name collisions? (though I'm not aware of any?)
  • Maybe just a bit too difficult to implement? perhaps revisit later? (though Contextual typing does not flow backward through function return types to the function arguments #5256 appears more difficult than this and seems to be in consideration?)
  • Or maybe this 'bends' the semantics of the constructor expression (new Class<T>()), or at least its default type arguments, in a way that's slightly unappealing from a "purist" point of view"? (though this doesn't seem like a "purist" language?). Perhaps some concept of "contextual baseline default type arguments" can be included as an integral part of the future proposal for default type arguments?
@malibuzios malibuzios changed the title Suggestion: use receiver's declared type arguments as defaults to its constructor Suggestion: use receiver's declared type arguments as defaults to its class constructor Apr 1, 2016
@malibuzios
Copy link
Author

I read somewhere you were using this concept of 'fresh' types in the compiler (I hope I'm not misusing it here but it did inspire me somewhat). What if the type of a new instance of a class type could be marked as 'fresh' as well? thus in:

let x: Map<string, number> = new Map();

The resulting type of the expression new Map() could be marked as 'fresh' and although it was in practice inferred as Map<{}, {}> it would be implicitly coerced to Map<string, number> during the assignment to x (I'm assuming here the constructed instance type would include the information on whether the arguments were baseline defaults and not explicitly typed as <{},{}>, of course, so that wouldn't be a problem).

However here:

let y = new Map();
let x: Map<string, number> = y;

Because the new instance was assigned to y, its type is not 'fresh' anymore thus it would not be coerced and the assignment may fail due to type incompatibility (i.e. Map<{}, {}> is not assignable to Map<string, number>).


I mentioned this example as well:

let createMap = () => new Map();
x: Map<string, number> = createMap();

Should the result of createMap() be marked as 'fresh' as well? I'm not sure if that would be desirable? Since the signature of createMap would be inferred as () => Map<{}, {}> that would be strange.

But What about this one?

let createMap = <V, U>() => new Map<V, U>();
x: Map<string, number> = createMap();

I have no idea? Is this pattern really that common or important?

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Apr 2, 2016
@RyanCavanaugh
Copy link
Member

"fresh" isn't really the right concept to use here, I think.

The more common pattern used to improve inference in cases like this is contextual typing. We have a concept of inference sites for generic type parameters which are formed by correspondences between the types of the arguments and their corresponding type parameters.

It should be sufficient to say that a generic contextual type e.g. T<U, V> can provide a type inference site for a corresponding new or call signature with a matching generic return type.

That would also solve cases like this:

interface MyMap<T> {
  doesSomething: T;
}
declare function createMap<T>(cb: (a: T) => T): MyMap<T>;

class Foo {
  x: MyMap<string>;
  constructor() {
    this.x = createMap(arg => arg.subtr(0)); // oops
  }
}

@malibuzios
Copy link
Author

@RyanCavanaugh

Thanks for the explanation! I was not even that sure what that 'fresh' concept really meant, so I guess I was just playing around with it a bit. I noticed that on discussions here you all use terminology like "type inference sites", "contextual types", "freshness", "narrowing", "widening" etc. maybe with time my understanding would sharpen but from a position of a user there are limits to how deeply I'd be able to go (I mean without spending an excessive amount of time on something which isn't really my "job"..).

(BTW my intention with Map example was more like a "hash table" (ES6 Maps and Sets..). I tried to find a simple example actually, not to make it any more complicated than it needs to by involving functions etc.. :) )

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this Effort: Difficult Good luck. and removed In Discussion Not yet reached consensus labels Apr 20, 2016
@RyanCavanaugh RyanCavanaugh added this to the Community milestone Apr 20, 2016
@RyanCavanaugh
Copy link
Member

Accepting PRs on this.

I tried to implement this with a somewhat naive approach and ran into problems with naked type parameters being inference candidates, which is bad. It'll take some effort to get this right. The test coverage we currently have seems sufficient to catch the weird cases, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Effort: Difficult Good luck. Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

2 participants