I'm trying to switch from using <array>.at()
to <array>.[]
for consistency. But here is a problem I have run into:
const array: string[] = [];
if (array[0]) {
const item = array[0]; // string
}
if (array.at(0)) {
const item = array.at(0); // string | undefined
}
if (array[0]) {
const item = array.at(0); // string | undefined
}
Why types are different? I'm missing something? I have read about at()
method but there not a lot of information about typescript.
CodePudding user response:
With those if (array[0])
-style type guards (and particularly since you've now said you have the noUncheckedIndexedAccess
option enabled), arguably the answer is just that TypeScript doesn't narrow types across function calls. After all, in the general case, the function can return a different value on every call (not all functions are pure).
You can lock it in with a const
:
// Note: `noUncheckedIndexedAccess` enabled
const array: string[] = [];
if (array[0]) {
const item = array[0];
// ^? −−−− string
console.log(array[0]); // string
}
const item1 = array.at(0);
if (item1) {
console.log(item1);
// ^? −−−− string
}
But that raises the interesting question of why without the guards are they different (if you don't have noUncheckedIndexedAccess
enabled)?
const array: string[] = [];
const i1 = array[0];
// ^? −−−− string
const i2 = array.at(0);
// ^? −−−− string | undefined
I suspect the reason is twofold:
Long ago, the TypeScript team took the pragmatic decision that (by default¹) indexed access into an array (like a
string[]
) should return the array's element type (string
), not the array's element type orundefined
(string | undefined
), even though the latter is a more correct interpretation of property access (which is what indexed access really is) and of the runtime reality that you could indeed getundefined
from it. Imagine how awkward TypeScript code would be if they hadn't! Every array access would involve an| undefined
that would just end up littering the code with non-null assertions or whatever. So they took the pragmatic approach.The
at
method has much more limited scope than indexed access in general — it's not at all intended to replace indexed access everywhere, and its primary use case is for when indexes are negative. So the types for it reflect the fact that it may returnundefined
, because that won't require non-null assertions everywhere — only in places where you useat
instead of indexed access. So there's no argument for not aligning with the spec and it's more useful for the types to closely align with the specification in this case.
¹ Re "by default": You can tell TypeScript you want indexed access to include | undefined
via the noUncheckedIndexedAccess
option. With that option enabled, my example above becomes:
// With `noUncheckedIndexedAccess` enabled
const array: string[] = [];
const i1 = array[0];
// ^? −−−− string | undefined <== Changed
const i2 = array.at(0);
// ^? −−−− string | undefined