Reputation: 1654
When looking at the inferred type of destructured element, it will assume the array is never empty.
const x: number[] = [];
const [first] = x; // first inferred as number
console.log(first); // undefined
if (first !== undefined) {
// ...
}
This leads to an interesting behavior with TSLint rule "strict-type-predicates" for example, that will mark the if statement as always true, while it's in fact isn't.
Am I missing something and it's normal behavior?
Upvotes: 16
Views: 3265
Reputation: 328292
It is still the intended behavior that undefined
is not included in the domain of index signature properties, for the reasons listed below. However, since this is an oft-requested feature, the TypeScript team has (somewhat grudgingly?) relented and added the --noUncheckedIndexedAccess
compiler flag. If you enable that (and it is neither enabled by default, nor included in the --strict
compiler flag, so you need to do it explicitly), you will start getting the behavior you presumably want.
Note that this is actually not exactly the same as adding undefined
yourself. Indexed access will add undefined
:
// --noUncheckedIndexedAccess is enabled
const x: number[] = [];
const [first] = x; // number | undefined
console.log(first); // undefined
if (first !== undefined) {
first.toFixed(); // okay
}
But for..of
loops and array functional programming methods will still act as though undefined
is not possible (that is: as if sparse arrays do not exist):
for (const n of x) {
n.toFixed(); // no error, yay!
}
x.map(n => n.toFixed()); // no error, yay!
So possibly you want to enable that flag. Keep in mind that some common array/dictionary manipulation techniques may still be "annoying", which is why it isn't part of the --strict
family.
It's the intended behavior. See microsoft/TypeScript#13778 for more information. This issue is a request to allow index signature property types to automatically include undefined
in their domain, and while the issue is still open, it is fairly clear that it will not be implemented. See this comment for example.
It is not a design goal of TypeScript (see #3 on the list) for the type system to be perfectly sound or correct (despite the heartache that people like me feel when we think about this too much; I've joked before about starting a TypeScript Unsoundness Support Group to help people deal with this). Instead, there's a tradeoff between correctness and usability.
The language maintainers note that there's a lot of real-world code out there which indexes into arrays without checking for possible undefined
values everywhere, and that enforcing this check would turn a simple for-loop into a tedious exercise of either performing checks or using type assertions. The problem is (see this comment) that the compiler cannot easily tell the difference between safe and unsafe indexes into array types. So either the compiler assume that the property won't be undefined
and have false negatives from the compiler when that assumption is wrong, as happens now.... or, assume the property might be undefined
and have false positives from the compiler when the indexing operation is actually safe. The argument by the language maintainers is that such false positives would happen so often that developers would condition themselves to ignore the errors entirely, thus making it just as useless as the current situation while being more annoying. So they will leave it as it is now. 😢
If you want, you can always add undefined
to the element type yourself, assuming such issues are more likely to show up for you in your use cases:
const x: (number | undefined)[] = [];
const [first] = x; // number | undefined
console.log(first); // undefined
if (first !== undefined) {
first.toFixed(); // okay
}
but keep in mind that you'll run into the annoying situation if your use cases follow the normal patterns:
for (const n of x) {
n.toFixed(); // error, annoying
if (typeof n !== "undefined") n.toFixed(); // okay
n!.toFixed(); // okay
}
x.map(n => n.toFixed()); // error, annoying
x.filter((n): n is number => typeof n !== "undefined").filter(n => n.toFixed()); // okay
x.map(n => n!.toFixed()); // okay
Okay, hope that helps; good luck!
Upvotes: 15