Skip to content

Type parameter cannot be inferred from another in simple case #35331

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

Closed
dgreensp opened this issue Nov 25, 2019 · 8 comments
Closed

Type parameter cannot be inferred from another in simple case #35331

dgreensp opened this issue Nov 25, 2019 · 8 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@dgreensp
Copy link

dgreensp commented Nov 25, 2019

TypeScript Version: 3.7.2 and 3.8.0-dev.20191123

Search Terms: Inference, generic function, unknown, argument not assignable, type parameter

Code

type Foo<T> = (t: T) => T
const increment: Foo<number> = x => x + 1

declare function acceptFoo<T>(foo: Foo<T>): void
acceptFoo(increment) // Works

declare function acceptFoo2<T, F extends Foo<T>>(foo: F): void
acceptFoo2(increment) // Error, argument not assignable to Foo<unknown>

// Work-around: explicitly intersect F with its bound
declare function acceptFoo3<T, F extends Foo<T>>(foo: Foo<T> & F): void
acceptFoo3(increment) // Works

Expected behavior:

Inference results in T being number, not unknown. No type error at acceptFoo2 call site.

Actual behavior:

T is unknown, causing a type error.

Note that if Foo were covariant in T, not invariant (as in this case) or contravariant, the above code would type-check, but the poor type for T could still be impactful, for example if T were part of the return type of acceptFoo. It's fortunate that the above work-around, using an intersection, is available. Without that, you'd have to specify all type parameters at the call site, because TypeScript doesn't yet support specifying some type parameters and inferring the rest. I am creating some APIs where it would be very inconvenient for the user to explicitly write out all the type parameters that would be inferred.

For some reason I thought TypeScript's inference was smart enough to go through one type parameter to another, but I tried this code on versions back to 2.8.1 and the result is the same.

Playground Link: http://www.typescriptlang.org/play/?ts=3.8.0-dev.20191123&ssl=1&ssc=1&pln=12&pc=31#code/C4TwDgpgBAYg9nAPAFQHxQLxQBTAFxTICUm6yAsAFADGcAdgM7BQCWd1AThALYR36wEiOgFduAIwgd0WAB6ko8gNRQAjFSoATCNQA2AQy5QAZiPbAW9KPurUIYYPCRpsxhAScpURAgDc4LJpUNnYOTthsnDx8wCQA9HFQAOpwHADWDBqU2nqG0KbmlnTWtvaOCABMKAA0sFAQssB8mgyCzqioru6wPlD+gcGlYZUR7Fy8-PGJAKIcHKm1hgDmYjFQdHDM+gwMLEt0+uK60MBwbYhmaRsA7nSoWQnJqWkAtIZwZpoEDWC6LNQsYC6ECsfhSBg6ZgwKDXQEAC1YwFa4g+dCC2R0BiMBWoFisITKTgAzDU6g0mmjWp40J03HAPEI0FAAGQ9PwBdEE4ZwImjKITWJQR4pdIMIA

Related Issues:

Possibly related to #35288, which is about inference not taking advantage of the fact that a class Foo extends interface IFoo.

@AlCalzone
Copy link
Contributor

Try this:

type Foo<T> = (t: T) => T
const increment: Foo<number> = x => x + 1

declare function acceptFoo2<F extends Foo<any>>(foo: F): void
acceptFoo2(increment) // Works
// ^ has type acceptFoo2<Foo<number>> (foo: Foo<number>): void

@dgreensp
Copy link
Author

dgreensp commented Nov 25, 2019

@AlCalzone In my real code, I actually do need to use T, for example I might need it in a bound for another parameter:

type Foo<T> = (t: T) => T
const increment: Foo<number> = x => x + 1

declare function acceptFooBar<T, F extends Foo<T>, S extends T>(foo: F, bar: S): S
acceptFooBar(increment, 2) // error

The work-around works just fine here:

type Foo<T> = (t: T) => T
const increment: Foo<number> = x => x + 1

declare function acceptFooBar<T, F extends Foo<T>, S extends T>(foo: F & Foo<T>, bar: S): S
acceptFooBar(increment, 2)

...which leads me to believe it could be inferred automatically.

@dgreensp
Copy link
Author

dgreensp commented Nov 25, 2019

I do sometimes use any in bounds, but I try to avoid it, especially since I realized that, ironically, this very issue makes it unsafe, with inference quietly setting type parameters to their bounds. So if you have Foo<any> as a bound of one parameter, and then you have another parameter, triggering this bug, now you might have a return value of any somewhere, even though you only used any in type parameter bounds! For example:

type Foo<T> = { value: T }
type Bar<F extends Foo<any>> = { foo: F } // seems reasonable enough...

declare function acceptBar<F extends Foo<any>, B extends Bar<F>>(b: B): F

const result = acceptBar({ foo: { value: 123 } }).value
// uh oh, result is now `any`, taken from the `any` in line 2

@AnyhowStep
Copy link
Contributor

The MyParamT & MyConstraintT "trick" is actually a thing I use fairly often, for when TS just can't infer the type properly, otherwise.

I'll have to dig through my projects to get specific examples.

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Nov 26, 2019
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Nov 26, 2019

There should never be more type parameters than inference sites; parameter constraints are not inference sites and are intended to not be inference sites. Generally if you find yourself writing multiple type parameters like this to try to relate the return type to some facet of the input type, you should use a conditional type or lift/lower the type parameter's position in the input position instead.

@dgreensp
Copy link
Author

dgreensp commented Nov 26, 2019

@RyanCavanaugh Thanks for your response. I think I understand what you are saying, basically that it's not the compiler's job to infer type parameters, but to infer things like argument types. Generally you should limit type parameters to the ones you need to describe the actual types of things, e.g. instead of:

declare function acceptBar<F extends Foo<any>, B extends Bar<F>>(
  b: B
): F

you would write:

declare function acceptBar<F extends Foo<any>>(
  b: Bar<F>
): F

or, you can extract the type parameter you need when you need it using a conditional, like so:

declare function acceptBar<B extends Bar<any>>(
  b: B
): B extends Bar<infer F> ? F : never

For this particular simple function signature, the second of the three snippets is clearly best.

My comments on this are:

  1. I'm uneasy about using any even in type parameter bounds, because it can leak out. I suppose that avoiding excess type parameters ought to reduce this problem — by hopefully avoiding cases where inference quietly decides not to do anything with a type parameter and just set it to its bound — but I don't feel confident that I know all the cases where a type parameter will be replaced by its bound. It's harmless for the checker to replace <T> with unknown or <T extends Foo<unknown>> (where Foo is covariant, say) with Foo<unknown>, but replacing <T extends Foo<any>> with Foo<any> seems very bad. I wish there was either A) a guarantee that type bounds won't leak into actual types, or B) something better than any for this purpose. I just read the strictAny thread, and I've read other threads proposing a new type alongside never, unknown, and any with interest. My intuition is that while any is a needed escape hatch in the language, an escape hatch shouldn't be needed here; there has got to be a better way. Really, I think I want some kind of existential types. But a starting point would be a type like any, call it some, that is assignable to and from but you can't just do what you want with it. A value x of type any has x.foo of type any, and x.bar of type any — it's like a magician's hat. I don't know if this is a necessary consequence of assignability rules or not; is there a way to make something like any with the same assignability but different results of indexing? Or is there a way to provide an escape hatch to variance but not assignability? If introducing a new type is strongly disfavored, I don't know what to say except anything is better than any.

  2. Conditionals work great for "one-way" type derivation, but if you want inference to go in both directions (i.e. from return type to arguments at some sites, and arguments to return type in others), I believe the "workaround" in this thread is useful. For example (keeping in mind that for this particular signature, we already know the best way to write it, which is not with a conditional, so you have to use your imagination), this inference does not work:

type Foo<T> = { value: T }
type Bar<F extends Foo<any>> = { foo: F }

declare function acceptBar<B extends Bar<any>>(
  b: B
): B extends Bar<infer F> ? F : never

// SUCCESS, result is 'number', type information flows from argument to return type
const result = acceptBar({ foo: { value: 123 } }).value

type MyFoo = Foo<(x: string) => void>
// FAIL, x is 'any', type information does not flow from return type to argument
const f: MyFoo = acceptBar({ foo: { value: x => console.log(x) } })

while, perhaps amazingly, this does:

type Foo<T> = { value: T }
type Bar<F extends Foo<any>> = { foo: F }

declare function acceptBar<F extends Foo<any>, B extends Bar<F>>(
  b: B & Bar<F>
): F

// SUCCESS, result is 'number'
const result = acceptBar({ foo: { value: 123 } }).value

type MyFoo = Foo<(x: string) => void>
// SUCCESS, x is 'string'
const f: MyFoo = acceptBar({ foo: { value: x => console.log(x) } })

Another problem with conditionals is they always succeed. The convention is to use never as a "failure" result, but sometimes this doesn't have the desired effect, which is to cause a type error.

  1. The success of intersecting a type parameter with its bound as in B & Bar<F> above, which is in accordance with @AnyhowStep's comment, suggests to me that this could be a possible improvement to inference.

I will keep the feedback in mind, and use fewer type parameters. The lack of a general way to write "Foo<T> for some unknown T" feels like a big pain point at the moment. I can write a generic function that takes a Foo<T> with parameter T, but then what if I want to capture the narrow type of the particular subtype of Foo<T> that was passed in, as well? I guess it comes down to what the inference sites actually are, and the bottom line is I can't rely on the inferencer to save me from any types by inferring type parameters.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Nov 26, 2019

There are cases where you have the same number of inference sites as type parameters and you still need the TypeParamT & TypeConstraintT trick. I just can't seem to find any examples of it at the moment...

Maybe I should revise my claim and say that I remember using this trick, but can't actually find these usages anymore...

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants