Code
type A = {
name: string
}
type B = {
id: number
}
function foo<T extends A | B>(target: T[]): T[] {
const res = [];
// const res: T[] = []; // Adding type annotation to res can resolve the error but why?
for (const e of target) {
res.push(e);
}
return res // TS think res is a type of (A|B)[] --> error!
}
function aoo<T extends A>(target: T[]): T[] {
const res = [];
for (const e of target) {
res.push(e);
}
return res // TS think res is a type of T[] --> no error!
}
Basically, I have two types A and B, and two generic functions foo
and aoo
.
The first function foo
has a generic type T
which is constrained by a union: A | B
A | B
, while the latter one is only constrained by A
.
Error
The error appears in foo
, and the reason is that TS thinks the result is a type of (A|B)[]
which is incompatible with T[]
. However, the return type of aoo
is inferred as T[]
as I expected. This is weird to me, I don't understand why TSC doesn't infer the return type of foo as T[
], and what's the difference between these two cases?
This is a side effect of the support added in Typescript 4.3 to contextually narrow values of generic types , as implemented by microsoft/TypeScript#43183 . There was a longstanding open issue at microsoft/TypeScript#13995 where control flow analysis would not work to narrow values of generic types constrained to union types , the same way it would work with values of specific types.
For example, the following always worked, where x
is of the specific union type string | number
string | number
:
function checkSpecific(x: string | number) {
if (typeof x !== "string") {
console.log(x.toFixed(2)); // okay
}
}
Here the fact that typeof x !== "string"
allows the compiler to narrow x
to number
and see that it has a toFixed()
method. But the following did not work before TypeScript 4.3, where x
is of the type T extends string | number
T extends string | number
:
function checkGeneric<T extends string | number>(x: T) {
if (typeof x !== "string") {
console.log(x.toFixed(2)); // error (before TS4.3)!
// ---------> ~~~~~~~
// Property 'toFixed' does not exist on type 'T'.
}
}
The compiler stubbornly refused to see that typeof x !== "string"
had any implications on the type of x
. One thing the compiler can't do is assume that the type parameter T
itself should be narrowed. After all, maybe T
really is the full union string | number
string | number
(eg, checkGeneric(Math.random()<0.5? "abc": 123)
) and so it wouldn't be right to narrow T
. But people who wrote the above code don't care about narrowing T
, they wanted x
to be narrowed from T
to number
.
And so with TypeScript 4.3, in certain situations when given values of generic types where the generic type parameter is constrained to a union, these values will first be widened all the way to the constraint and then narrowing can happen:
function checkGeneric<T extends string | number>(x: T) {
if (typeof x !== "string") {
console.log(x.toFixed(2)); // okay (TS4.3 and above)
}
}
The compiler has decided to take x
and see its type not as the generic type T
, but as the specific type string | number
string | number
to which T
is constrained. And once it does this, then typeof x !== "string"
can narrow x
to number
as desired.
Unfortunately, in your code, the same analysis leads to surprising behavior. Prior to TypeScript 4.3, there would be no error:
function foo<T extends A | B>(target: T[]): T[] {
const res = []; // <-- auto typed
for (const e of target) {
res.push(e); // <-- res is inferred as T[] before TS4.3
}
return res // okay
}
The variable res
is considered to be an "auto-typed" or "implicit any
" variable because the compiler cannot use its initializer to infer the type; it needs to wait to see what you do with it and then evolve the type based on that. For arrays like res
this was implemented in microsoft/TypeScript#11432 .
Before TypeScript 4.3, when you called res.push(e)
, the compiler would see that e
is of type T
, and thus res
is now evolved to be of type T[]
, and then return res
is fine.
But starting with TypeScript 4.3, this has changed:
function foo<T extends A | B>(target: T[]): T[] {
const res = []; // <-- still auto typed
for (const e of target) {
res.push(e); // <-- res is inferred as (A | B)[] starting with TS4.3
}
return res // error!
}
The res
variable is still auto-typed and evolves when you call res.push(e)
. But because the value e
is of a generic type constrained to a union, the compiler uses its new behavior to first widen e
from T
all the way to the constraint A | B
A | B
. And that means that res
's type is (A | B)[]
and you get an error. Since you never tried to narrow e
from A | B
A | B
to either A
or B
in this code, the added support for control flow analysis is completely useless for your purposes.
Oh well.
Note that neither the old nor the new behavior is incorrect ; a value of type T
where T extends XXX
can be widened to XXX
safely. It's just that there are some situations in which T
will be more or less useful than XXX
. The heuristics added in TypeScript 4.3 improved things for a lot of situations, but unfortunately made things worse in others. I'd be interested in seeing what would happen if someone filed an issue about this, but I wouldn't go so far as to call it a bug.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.