Home > OS >  How can I get Typescript to infer the correct type from a generic without using cast?
How can I get Typescript to infer the correct type from a generic without using cast?

Time:02-02

The interpreter is smart enough to infer that the result of the filter function will be an array of strings, yet it thinks that is not assignable to the variable b of the same type? Is there any way to solve this without being forced to do

const b: Arr<T> = arr.filter(() => true) as Arr<T>

enter image description here

CodePudding user response:

This answer is not canon but I hope it helps build a robust enough mental model.

Breaking it down to multiple versions will disambiguate it a little bit:

// I modified `Arr` for the sake of the explanation
type Arr<U> = U extends any ? U[] : U[]

function foo <T extends string>(arr: Arr<T>): T[] {
  const b = arr.filter(() => true);
  return b;
// ~~~~~~~~
// Type 'string[]' is not assignable to type 'T[]'.
//   Type 'string' is not assignable to type 'T'.
//     'string' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'string'.
}

function bar <T extends string>(arr: T[]): Arr<T> {
  const b = arr.filter(() => true);
  return b;
// ~~~~~~~
// Type 'T[]' is not assignable to type 'Arr<T>'
}

function quux <T extends string>(arr: T[]): T[] {
  const b = arr.filter(() => true);
  return b;
}

TS can't process Arr<T> if T is a generic because its value is "not known" and therefore "there is no way" to tell if U extends any or not.

Now you're going to tell me that TS knows that T extends string and should come to the conclusion that string does not extend any[]. It is even more ridiculous with my rewrite because everything extends any and we return U[] anyway.

TS takes neither the constraint of T nor the two arms of the conditional into account to make any kind of reasoning: either it decides that T is "not known" and gives up, or it has replaced T by its constraint at some point, then it will compute the return value but T is no longer a generic and is less useful.

Now, what our examples highlight is that TS does not treat generics consistently:

  • You can tell that in foo, it substituted T with its constraint, which explains that there is a disconnect between the input value and the return value.

  • In the case of bar, TS did not do any substitution, it simply gave up and therefore cannot conclude that anything extends Arr<T>.

So it appears that TS infers the type of each expression individually and does not propagate type information across the entire function. That's weird but if you think about it, it probably allows it to infer narrow return types more often than if it did.

Interestingly, in your code TS would process Arr<T[]> because even though it does not know the value of T, it knows that it's wrapped in an array and can proceed to compare that with any[] and return the right type. Here for some reason you designed Arr to flatten its argument so it would work although I don't recommend you do that.

If your use case is real, then quux is what you should do. I don't know why you designed Arr the way you did though.

I also want to point out another inconsistency:

const id = <T>(x: T) => x;

function foobar <T extends string>(arr: Arr<T>): Arr<T> {
  const b = id(arr)
  return b;
}

Here foobar compiles. I think the reason is because the type of arr.filter is the following:

Array<T>.filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[]`

The generic is not on the function but on the object, which seems to force the resolution of arr, as if TS needed to know what Arr<T> was so that it can produce the type of Arr<T>['filter']. This is just an assumption.

  • Related