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

Confusion with generics and functions #14824

Closed
johnfn opened this issue Mar 23, 2017 · 8 comments
Closed

Confusion with generics and functions #14824

johnfn opened this issue Mar 23, 2017 · 8 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@johnfn
Copy link

johnfn commented Mar 23, 2017

Here's some code. The first one has two arguments, both of which are type T. The type catcher catches that type T is different.

const fn1 = <T>(a: T, b: T) => { }
fn1({ a: 1 }, {}) // Good: a is missing in type {}

Now, the first T is inferred from the arguments of a passed function, and the type checker does not catch that T is different.

const fn2 = <T>(inner: (args: T) => void, args: T) => { }
fn2((args: { a: 1 }) => { }, {}) // no error..?

What's going on here?

More importantly, I would really like to be able to say the type of the 2nd argument is the arguments to the first. How can I say that in a way that catches missing properties?

@altschuler
Copy link

altschuler commented Mar 23, 2017

A workaround would be

const fn2 = <T, U extends T>(inner: (args: T) => void, args: U) => { }
fn2((args: { a: 1 }) => { }, {}) 

which gives the correct error Property 'a' is missing in type '{}'.

@johnfn
Copy link
Author

johnfn commented Mar 23, 2017

Yep, that workaround is perfect. Thank you!

Still curious about the original code though.

@RyanCavanaugh
Copy link
Member

Inference for T produces { }, but the function literal's type is assignable to the declared type, so no error occurs. See https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-function-parameters-bivariant and note that this example is an error:

const fn2 = <T>(inner: (args: T) => void, args: T) => { }
fn2((args: { a: 1 }) => { }, { a: '' }) // no error..?

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Mar 23, 2017
@RyanCavanaugh
Copy link
Member

Logged #14829 which is tangentially related (but wouldn't actually solve this)

@johnfn
Copy link
Author

johnfn commented Mar 27, 2017

Thanks, @RyanCavanaugh. I still don't understand why T doesn't infer to be the more specific of the two types, as it appears to do in fn1.

@RyanCavanaugh
Copy link
Member

Inference works by the following process (greatly simplified):

  • Gather all the places where T is used
  • Pick a usage of T whose type is a supertype of all the other types
  • If no type is a supertype of all the others, issue an error

The fn1 case is complicated by the fact that the expression { a: 1 } has a "fresh" type. When a type is "fresh" it has slightly different subtype rules that ultimately make the fresh object type { a: 1 } not be a subtype of { } (normally all types are subtypes of { }). In the fn1 case we fail to find a common supertype and issue an error for that reason, rather than erroring that { } is missing a (that error is displayed because it's the root cause of failing to find a common supertype).

A more demonstrative example is this, where we assign to j to remove the freshness:

const fn1 = <T>(a: T, b: T) => { }
const j = { a: 1 };
const k = {};
// T: { }
fn1(j, k);

Note that it'd be wrong to pick the most specific type among the candidates during inference - you wouldn't want choose(dog, animal) to error because Animal isn't a subtype of Dog, for example.

@johnfn
Copy link
Author

johnfn commented Mar 28, 2017

Thanks for the great explanations as always, @RyanCavanaugh.

The thing I'm not understanding is it seems rather subjective whether you'd choose the most specific type in the example you gave. For example, if fn1 was something like assertEqual<T>(a: T, b: T), you would indeed want T to be the most specific type of the two. It should be a type error to try to assert the equality of an Animal and a Dog, for instance, because you clearly passed in one of those arguments incorrectly.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 28, 2017

That's not at all obviously an error?

var zoo = new Zoo();
var dog = new Dog('spot');
zoo.add(dog);
zoo.add(new Cat('fluffy'));
var a = zoo.getAnimalByName('spot'); // a: Animal
assertEqual(a, dog);

@mhegazy mhegazy closed this as completed Apr 21, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 21, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

4 participants