TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Array access possibly undefined with noUncheckedIndexedAccess despite findIndex

Open Sainan opened this issue 10 months ago • 11 comments

🔎 Search Terms

noUncheckedIndexedAccess, findIndex, undefined

🕗 Version & Regression Information

Tried 5.8.2 and nightly (5.9.0-dev.20250310).

⏯ Playground Link

https://www.typescriptlang.org/play/?noUncheckedIndexedAccess=true&ts=5.8.2#code/MYewdgzgLgBAhgJwQLhmArgWwEYFMEDaAujALwwECMANDAEy0DMRA3AFCiSwCWAJgB5l4SAHQAzbmF4BJKbn4AKQaQB8MZeToBKdtzEwFfQQEJyAWkpaYAbzYx7MTtHWoMOfEMSEjrOw6cgADa4IoEgAOZKOmwAvmxAA

💻 Code

const arr: number[] = [1, 2, 3];
const idx = arr.findIndex(x => x == 2);
if (idx != -1) {
    const x: number = arr[idx];
    console.log(x);
}

🙁 Actual behavior

Type 'number | undefined' is not assignable to type 'number'.
  Type 'undefined' is not assignable to type 'number'.

🙂 Expected behavior

Code 'compiles' without errors.

Additional information about the issue

No response

Sainan avatar Mar 10 '25 08:03 Sainan

This is working as intended. findIndex() does not narrow the type of arr, which would be necessary to allow unchecked index access.

MartinJohns avatar Mar 10 '25 08:03 MartinJohns

Well, of course it doesn't narrow the arr, but it guarantees that idx == -1 || (idx >= 0 && idx < arr.length), and given that I checked idx != -1, it is now guaranteed that idx is in range of the array, which because it is number[], should yield that x is number and not number | undefined.

Sainan avatar Mar 10 '25 08:03 Sainan

If you consider this too complex, there's also the simpler case of

const arr: number[] = [1, 2, 3];
const idx: number = ...;
if (idx >= 0 && idx < arr.length) {
    const x: number = arr[idx];
    console.log(x);
}

which fails with the same error.

Sainan avatar Mar 10 '25 08:03 Sainan

It's just a limitation of how TypeScript is designed. It needs to narrow the type, otherwise the check does not work. There's no hardcoded special behavior for findIndex(), it's just a method returning a number, nothing indicates that retrieving this value and checking it means the index access is valid.

I suggest you to write the code this way:

const arr: number[] = [1, 2, 3];
const idx: number = ...;
const x: number | undefined = arr[idx];
if (x !== undefined) {
    // ...
}

MartinJohns avatar Mar 10 '25 09:03 MartinJohns

What I'm saying is that there's guarantees about the range of idx which influence how indexing an array with the number will behave.

I don't think it's that much different from narrowing a range of possible string values, e.g. like here:

type Rarity = "COMMON" | "UNCOMMON" | "RARE";
const probabilities: Record<Exclude<Rarity, "RARE">, number> = { COMMON: 80, UNCOMMON: 20 };
declare function getKey(): Rarity;
const key: Rarity = getKey();
if (key != "RARE") {
    const probability: number = probabilities[key];
    console.log(probability);
}

Sainan avatar Mar 10 '25 09:03 Sainan

I don't think it's that much different from narrowing a range of possible string values, e.g. like here:

Completely different case. Here the key can be narrowed to "COMMON" | "UNCOMMON", and according to the type of probabilities it always has a COMMON and an UNCOMMON property, so accessing is valid.

What I'm saying is that there's guarantees about the range of idx which influence how indexing an array with the number will behave.

And I'm telling you: The compiler doesn't know about this guarantee. There's nothing about the signature of findIndex() that lets the compiler know that the idx is valid to access.

MartinJohns avatar Mar 10 '25 10:03 MartinJohns

Well, in the case of if (idx >= 0 && idx < arr.length), the guarantee is given by the branch. Updating findIndex typings to provide that guarantee can be done later.

Sainan avatar Mar 10 '25 10:03 Sainan

Well, in the case of if (idx >= 0 && idx < arr.length), the guarantee is given by the branch.

This can't be done either. length is just a number property, nothing about it indicates that a later indexed access will succeed if checked. See #38000.

You need to think about it on a type level, not on a runtime level. You see the code and think yourself "yeah, of course this will be fine when running", but how would the compiler know about this by looking at the types?

MartinJohns avatar Mar 10 '25 10:03 MartinJohns

I would recommend turning off noUncheckedIndexedAccess if you're always confident that your accesses are in-bounds. It is intentionally a blunt instrument and we don't intend to add new kinds of complex CFA which would weaken its soundness.

https://github.com/microsoft/TypeScript/issues/13778#issuecomment-286592106

Not directed at OP: I kept trying to warn people this would happen 🫠

Image

Image

RyanCavanaugh avatar Mar 10 '25 17:03 RyanCavanaugh

FWIW, I personally am only looking into adoption of this flag to reduce the number of false-positives I'm getting from eslint's no-unnecessary-condition rule which is otherwise very optimistic about e.g. indexing on Record<string, ...> always being a hit, and on that side of the ecosystem they also have no interest in making the tooling more useful/eliminating false-positives. See https://github.com/typescript-eslint/typescript-eslint/pull/10921.

Sainan avatar Mar 10 '25 17:03 Sainan

on that side of the ecosystem they also have no interest in making the tooling more useful/eliminating false-positives

Apologies for the drive-by nitpicking, but: we do have interest! We're just blocked from doing anything ourselves. The situation is that we directly use TypeScript's inference. Anything like the requested "do this one specific case better" would need to be solved at the TypeScript level. Asking typescript-eslint to do better here is like asking your coffee mug to give you fresher coffee beans. It's a delivery method, not the roaster itself. 😛

JoshuaKGoldberg avatar Mar 10 '25 17:03 JoshuaKGoldberg

This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

typescript-bot avatar Mar 13 '25 01:03 typescript-bot