15

In TypeScript, if strict null checking is enabled, I would expect the compiler to prevent me from assigning null or undefined values to a variable unless it admits null.

However, array access seems to allow circumventing this check.

Example:

let a: string[] = ["Hello"];
let s: string;

// 1) this produces an error, as expected
s = undefined

// 2) s is undefined here, too, but no error
s = a[3];
console.log(s);

Runnable version on the TypeScript Playground (Note: "strict null checking" must be enabled in the "Options" dialog).

What is going on here?

  • Is this a bug in the TypeScript compiler?
  • Or is it a deliberate omission?
  • If the latter, is this documented anywhere (ideally with a rationale why it was done) ?
sleske
  • 81,358
  • 34
  • 189
  • 227
  • 1
    Here you go: https://github.com/danielnixon/eslint-plugin-total-functions – danielnixon Jun 09 '20 at 11:57
  • @danielnixon: Interesting - obviously I'm not the only one who noticed. – sleske Jun 09 '20 at 13:50
  • @danielnixon: Consider writing an answer based on your project. It seems to me that using your plugin is a valid solution (or at least workaround) if you want to avoid arrays. – sleske Jun 09 '20 at 13:58

2 Answers2

18

Found it :-).

tl;dr: It is a deliberate omission. Array access is very common in TypeScript code, and forcing a null/undefined check for every access was considered too cumbersome for developers.

Note that since TypeScript 4.1 this is configurable with the compiler option noUncheckedIndexedAccess. See Klaster_1's answer for details.


The issue has been raised several times in discussions:

The comment on PR 7140 has a nice rationale from Anders Hejlsberg (one of the core developers):

Indexing simply produces a value of the type declared in the matching index signature. Even if it is technically more correct, it would simply be too painful if we automatically added undefined to the type of every indexing operation.

For example, every array element access would have to be accompanied by a non-null guard or a ! assertion. I think that would become hugely irritating.


Personal comment: I think this is a reasonable decision. The problem is inherent with arrays - I don't think it's possible to prove (for the compiler) whether a given position is defined, since you can use calculated positions. So any warning would produce many false positives, and most devs would switch it off anyway.

Essentially, arrays provide more freedom to the developer than the compiler can check. If you want proper checking, the only solution I can see is to avoid direct array access, and use other data structures that provide more protection.

sleske
  • 81,358
  • 34
  • 189
  • 227
  • 3
    Very unfortunate – drkibitz Feb 07 '20 at 22:23
  • 4
    This is really a poor decision for the language IMO. There are so many things that are hugely irritating in Typescript - and for good reason! I will take safe and irritating over unsafe any day. – Alex Neth Apr 29 '20 at 04:55
  • @AlexNeth: Actually, I think it's a reasonable decision. The problem is inherent with arrays - I don't think it's possible to prove (for the compiler) whether a given position is defined, since you can use calculated positions. So any warning would produce many false positives, and most devs would switch it off anyway. If you want proper checking, you'll have to use some other data structure (e.g. a list that only allows appending, not arbitrary writing). – sleske Apr 29 '20 at 12:22
  • 2
    I feel like foo?[0] would be just fine in places where users want to instruct the compiler it shouldn't try. Or that at least there should be a typescript type of array with strict checking. – Alex Neth Apr 30 '20 at 09:08
  • Great answer. Thank you. – Denis Pshenov Dec 08 '20 at 06:18
  • the typescript compiler can't know if a position is defined, but eslint's "no unnecessary condition" rule will complain if you check for defined-ness. this is why we can't have nice things – Agos Nov 21 '22 at 08:57
8

TypeScript 4.1 introduced a new compiler option - noUncheckedIndexedAccess. Among other things, it adds undefined to array index access type.

Consider the following snippet (TS playground):

const items = [1,2,3]
console.log(items[1]?.toString(10))
console.log(items[2].toString(10))

Without noUncheckedIndexedAccess, items[2].toString(10) will be deemed valid, and invalid when the option is on. The items[1]?.toString(10) will be valid when the option is on, just like in comment by Alex Neth in the older answer.