Home > Enterprise >  Why <array>.[n] type are different from <array>.at(n)
Why <array>.[n] type are different from <array>.at(n)

Time:05-01

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.

[email protected]

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
}

Playground link


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

Playground link

I suspect the reason is twofold:

  1. 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 or undefined (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 get undefined 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.

  2. 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 return undefined, because that won't require non-null assertions everywhere — only in places where you use at 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

Playground link

  • Related