In the following code snippet, why does foobar
revert to being of type Foobar
when it is referenced from the function passed to filter
?
type Foo = { type: "foo"; foo: number };
type Bar = { type: "bar"; bar: number };
type Foobar = Foo | Bar;
const foobars: Foobar[] = [
{ type: "foo", foo: 42 },
{ type: "bar", bar: 43 },
];
const numbers = [40, 41, 42, 43, 44];
function logFoo(foo: Foo) {
console.log(foo.foo);
}
for (let foobar of foobars) {
if (foobar.type === "foo") {
console.log(foobar.foo); // fb is Foo
logFoo(foobar); // OK
console.log(numbers.filter(x => x < foobar.foo)); // Property 'foo' does not exist on type 'Foobar'
}
}
If you change let foobar of foobars
to const foobar of foobars
the type error goes away.
CodePudding user response:
If you're in a hurry, the answer is to use const
.
for (const foobar of foobars) {
if (foobar.type === "foo") {
console.log(foobar.foo); // fb is Foo
logFoo(foobar); // OK
console.log(numbers.filter(x => x < foobar.foo)); // Property 'foo' does not exist on type 'Foobar'
}
}
A const
(or 'final', as Java calls it) variable will never be reassigned. That gives us some very strong guarantees. Inside the if
statement, we know absolutely that the value foobar
has type Foo
. And since it will never be reassigned, we also know that inside of any closures within the if
statement. You get the behavior you're expecting.
Now consider let
.
for (let foobar of foobars) {
if (foobar.type === "foo") {
console.log(foobar.foo); // fb is Foo
logFoo(foobar); // OK
console.log(numbers.filter(x => x < foobar.foo)); // Property 'foo' does not exist on type 'Foobar'
}
}
A let
-bound variable can change. TypeScript is smart enough to see that, inside the if
statement, it hasn't changed. But when it's passed to a closure, that closure could be called from anywhere else in the code. If someone were to come along and assign a new value to this variable somewhere unrelated in the code, then the closure's type safety would break. So the closure makes the coarsest and safest guarantee it can, which is that the let
-bound variable is of its original type.
In this particular example, it's theoretically possible that TypeScript could reason about the fact that our foobar
is never assigned to at all. After all, its scope is limited to the inside of a for
loop, and there are no such assignment statements in there. But rather than get that far into the weeds (the scope of a variable could be an entire file, after all, or in the case of instance variables could be multiple files), TypeScript decides to just be conservative.
Note that if you do intend to modify the variable later but want to capture its particular value at a moment, rather than the variable itself, you can always make a new const
-bound variable to do so.
for (let foobar of foobars) {
if (foobar.type === "foo") {
const newFoobar = foobar;
console.log(numbers.filter(x => x < newFoobar.foo)); // Property 'foo' does not exist on type 'Foobar'
}
}
At the moment newFoobar
is created, foobar
has type Foo
. And newFoobar
is a const
, so it will always have type Foo
. Nothing can change that fact beyond this point, so it's safe to capture newFoobar
(as type Foo
) inside of a closure.
CodePudding user response:
Silvio's answer is correct but does not fix your code as you might want to use Bar
in your loop (as mentionned, using const
will result in foobar
being a Foo
).
Therefore i would recommand to use a forEach
(or a for in
using let
) on foobars like
foobars.forEach((foobar: Foobar) => {
if (foobar.type === "foo") {
const myFoo: Foo = <Foo> foobar;
logFoo(myFoo); // OK
console.log(numbers.filter(x => x < myFoo.foo));
} else if (foobar.type === "bar") {
const myBar: Bar = <Bar> foobar;
console.log(numbers.filter(x => x > myBar.bar));
}
});