See microsoft/TypeScript#30406 for a canonical answer.
For the question, “why does the compiler think that pop()
might return undefined
even when I check the array’s length
“, the short answer is “because the TypeScript standard library’s call signature for the pop()
method on Array<T>
returns T | undefined
“:
interface Array<T> {
// ... elided
pop(): T | undefined;
// ... elided
}
Thus whenever you call pop()
on an array, the type of the return value will include undefined
.
The logical next question is “why don’t they make better call signatures which return T
if the length
is nonzero and undefined
if the length
is zero?” The answer to that is “because checking the length
property of a general array type doesn’t change the apparent type of the array, so the call signatures can’t tell the difference”.
You could, for example, add a few call signatures like this:
interface Array<T> {
pop(this: { length: 0 }): undefined;
pop(this: { 0: T }): T;
}
By using this
parameters, each call signature will only be selected when the array on which the method is called matches the specified type. If the array has a length
of 0
, return undefined
. If the array has an element of type T
at the key 0
, return T
.
This will work well for tuple types whose length is fixed and whose element indices are known:
declare const u: [];
const a = u.pop(); // undefined
declare const v: [1, 2];
const b = v.pop(); // 1 | 2
But of course calling pop()
on a tuple is a bad idea and not the kind of thing you’d be likely to want. If you have a variable-length array and call pop()
on it, neither call signature is selected and you fall back to the built-in T | undefined
, even if you try to check the length
:
const w = Math.random() < 0.5 ? [] : ["a"] // string[]
if (w.length) {
w.pop().toUpperCase(); // error! Object is possibly undefined
}
The problem is that the length
property of an Array<T>
is of type number
, and there’s no “nonzero number
” type to narrow w.length
to. In order to support this kind of thing, you’d need something like negated types which are not part of TypeScript. It’s possible that with sufficient compiler work, someone could give enough structure to arrays in TypeScript that a truthiness check on w.length
would narrow down the type of the array to something you can call pop()
on without worrying about getting undefined
out.
But it would add a ton of complexity to the compiler and the language to support this use case. The benefit is not likely to outweigh the cost.
In the absence of this, it’s so much easier for you to skip the length
check and just call pop()
, checking to see whether it’s undefined
or not. This is the same amount of work for you, since it’s just moving the check from before to after the call, and it makes things more straightforward for the compiler.
The other answers posted here suggest this and other workarounds, so I won’t go into them. My main point is that the language is just not equipped to allow a length
check to affect the behavior of pop()
. Or, as @RyanCavanaugh (dev lead of TS) commeted in microsoft/TypeScript#30406,
This isn’t something we’re capable of tracking
Oh well!