EcksDy
EcksDy

Reputation: 1654

TS will not infer possible undefined when destructuring empty array

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) {
    // ...
}

Playground Link

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

Answers (1)

jcalz
jcalz

Reputation: 328292

UPDATE for TypeScript 4.1

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.

Playground link to code


ORIGINAL ANSWER

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!

Playground link to code

Upvotes: 15

Related Questions