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>
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 substitutedT
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 extendsArr<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.