Home > Enterprise >  Typescript with strict mode shows error "Object is possibly 'undefined'" but it
Typescript with strict mode shows error "Object is possibly 'undefined'" but it

Time:06-02

Consider the following short example:

type User = {
  id: number;
  name: string | undefined;
}

export function testUser(user: User) {
  const userIsValid = !!user && !!user.name;
  if(userIsValid) console.log(user.name.length);
}

And strict mode actived in tsconfig.json:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

When I run tsc I got an error:

index.ts:8:31 - error TS2532: Object is possibly 'undefined'.

8   if(userIsValid) console.log(user.name.length);
                                ~~~~~~~~~


Found 1 error in index.ts:8

error Command failed with exit code 2.

But everyone know that is impossible to be undefined inside that if clause.

Is it a bug?

CodePudding user response:

Generally speaking, TypeScript can't narrow the type of one variable/property based on another variable/property,¹ even when to us humans it's (fairly) obvious that (in this case) userIsValid is only true when user and user.name are both truthy. It just doesn't do analysis to that level. However, in some situations, TypeScript 4.4 and above can do that analysis — see jcalz's answer for how you could do that with Readonly.

But before v4.4 or if you can't use a readonly object, use the variable itself:

type User = {
    id: number;
    name: string | undefined;
};

export function testUser(user: User) {
    if (user.name) {
        console.log(user.name.length);
    }   
}

Playground link

(Note there's no need to test user. As Mike S. said in a comment, it's not optional or nullable, so you can trust that it refers to a User object.)


¹ It can narrow the type of an object based on an object property, and narrow types in various ways, but not the kind of thing you're doing in your question.

CodePudding user response:

TypeScript 4.4 introduced support for control flow analysis of aliased conditions like your userIsValid boolean value. However, because tracking such dependencies in general could be very taxing on the compiler, it only works in certain situations. The name property of User is not a readonly property, so it is possible someone could write user.name = undefined after your userIsValid test. This possibility means the compiler gives up on narrowing in your code, since otherwise it would need to check that such an assignment does not occur, which is expensive in the general case.

An easy enough fix for that is to make user a read-only version of User, e.g., a Readonly<User> using the Readonly<T> utility type:

export function testUser(user: Readonly<User>) {
  const userIsValid = !!user && !!user.name;
  if (userIsValid) console.log(user.name.length); // okay
}

That works because the compiler is now at least somewhat confident that your code won't reassign user.name anywhere, and so userIsValid is the sort of aliased condition it will track.

Note that, in general, a User and a Readonly<User> are mutually assignable; a readonly property is a declaration of developer intent that it will not be reassigned in the relevant scope; it's not a guarantee of immutability. So the compiler doesn't prevent you from passing a User into a function that wants a Readonly<User>:

const u: User = {id: 123, name: "alice"};
testUser(u); // okay

which is good, since it means the change to testUser() I made has no annoying effects on callers.

Playground link

  • Related