TypeScript: Object is possibly undefined` error in TypeScript 4.1.2

TypeScript Version: 4.1.2

Search Terms: Object is possibly undefined

Code

function main(values: Array<string>) {
    for (let i = 0; i < values.length; i++) {
        if (typeof values[i] === 'string' && values[i].length > 0) {
            console.log(i);
        }
    }
}

Expected behavior: No error

Actual behavior: TypeScript 4.1.2 reports a Object is possibly undefined error in values[i].length but given the first condition the object must be defined as it is of type ‘string’

Playground Link: https://www.typescriptlang.org/play?noUncheckedIndexedAccess=true&ts=4.1.0-beta#code/GYVwdgxgLglg9mABAWwIYzACgG6oDYgCmAzgFyICCATlagJ4A8xUVGA5gHwCUiA3gFCIhiYHCqJMeQlEQxEAXkQAGANyzEDRLgIkAdFLBsoACzUwA1OZ4Dht2cAlQ6AB0JwH2osQDaMALoK8ooA5MyshsGIAGRRWvhevn76hIYmiBzK1oJ2ORAIxHBS+nBsmDBcKtk5AL5ViLW1QA

Related Issues: no

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 22 (8 by maintainers)

Most upvoted comments

@RyanCavanaugh this should work as well

function main(values: ReadonlyArray<string>) {
    for (let i = 0; i < values.length; i++) {
        if (typeof values[i] === 'string' && values[i].length > 0) {
            console.log(i);
        }
    }
}

since ReadonlyArray can’t magically change what indexes are in the set and what are not

This was one of the caveats that kept the flag from being implemented for so long. We don’t have any way to ascertain whether i or the indexed array has changed between any two arbitrary expressions, so the only “safe” thing to do is always include undefined. You can write

function main(values: Array<string>) {
    for (let i = 0; i < values.length; i++) {
        const el = values[i];
        if (typeof el === 'string' && el.length > 0) {
            console.log(i);
        }
    }
}

@radekmie

  1. Your code is valid JavaScript, but TS already disallow it.
  2. I have already said “bar in this case” so I just want to focus on this case
  3. I think your comment is more like “TS shouldn’t do anything that’s not 100% safe”, however in my second theory TS has already accepted something “unsafe”, so the point is “what’s the possiblility for developers (at all skill levels) to make such a mistake” and “is it suitable to treat all such scenario as an error”

The entire point of the flag was to be more safe than TS’s normal behavior, so it being more restrictive than the default is a not undesirable aspect.

I appreciate having that option, but it mixes together two different concerns in an inseparable way

  1. safety in the face of mutating data
  2. safety for indexed array access (even on immutable data and index)

I think there is a good chunk of people that would like having option 2 without also being forced into option 1 (as it is much more rare, while 2. is very common).

Even so, it is a valuable improvement, just not as useful as it could be.

since ReadonlyArray can’t magically change what indexes are in the set and what are not

This isn’t true. A ReadonlyArray might be an alias for an underlying mutable array. e.g.

function fn(cb: (arr: ReadonlyArray<number>, clear: () => boolean) => void) {
  let arr = [1, 2, 3];

  cb(arr, () => {
      while(arr.length) arr.pop();
      return true;
  }) 
};

// Crashes
fn((arr, clear) => {
    let i = 0;
    if (arr[i] !== undefined && clear() && arr[i].toFixed() === "1") {

    }
})

@yume-chan I just wanted to emphasize that being const-constant is not enough, that’s all. I agree with 3 though.

@peterholak Option two would require a way to represent immutable types in TypeScript. This is currently not possible. It’s something I personally really want, but it’s nothing in the foreseeable future. Issues to follow would be #14909, #16317, #17181.

I know TS doesn’t recognize this pattern, even for normal optional properties:

interface Foo {
  bar: string | undefined;
}

declare const foo: Foo;

if (foo.bar) {
  foo.bar.toLowerCase(); // OK
}

const bar = 'bar';
if (foo[bar]) {
  foo[bar].toLowerCase(); // Object is possibly 'undefined'.
}

(Playground link)

However I can’t understand why:

If it’s because a variable (bar in this case) may change between two accesses, since bar is a constant, it’s not possible.

If you say the object (foo in this case) may change, then why does accessing with literals work? I expect they (access with literals and variables) both work or not work, and I prefer both not.

The entire point of the flag was to be more safe than TS’s normal behavior, so it being more restrictive than the default is a not undesirable aspect.

@doberkofler Consider the following (contrived) example though

function main(values: Array<string>) {
    for (let i = 0; i < values.length; i++) {
        const incrementI = () => {i = i + 1;}
        if (typeof values[i] === 'string' && incrementI() && values[i].length > 0) {
            console.log(i);
        }
    }
}

values[i].length is no longer guaranteed to be defined, and TS has no good way to detecting that change.

@thecotne Just to make sure: it should but it does not. Correct?