1

This is the MWE:

interface Animals {cat:string, dog:string}

let a: Animals|undefined;


if(a){// if we got `a` from somewhere
 [1,2,3].map(v=>a.cat)
 // ----------> ~ Object is possibly 'undefined'
}

Playground

I am not sure what exactly the error is, it seems to be related to map outputting something or undefined, but why a isn't defined?

2
  • You didn't describe the actual error reported by TS; it's that a might be undefined, not a.cat. I'm going to edit so that this is actually a minimal reproducible example Commented Nov 20, 2022 at 16:48
  • Also slightly perplexed by this as a should be inferred as being Animals within the scope of the if statement. Assigning a.cat to a variable and using that variable in the map is accepted by the compiler (typescriptlang.org/play?#code/…). It is beyond my understanding why this is but thought I would share for future commenters Commented Nov 20, 2022 at 16:52

2 Answers 2

2

This is a general limitation of TypeScript, described in microsoft/TypeScript#9998. The effects of control flow analaysis, such as the narrowing of a from Animals | undefined to Animals following the truthiness check if (a), do not persist to closed-over values across function boundaries.

Inside the body of the callback v => a.cat, therefore, there has been no control flow narrowing of a; it is considered to be of type Animals | undefined, and therefore you get an error when indexing into it.

The reason this happens is because there is currently no way for the compiler to know that the callback v => a.cat is run immediately. That's out-of-band information we have about Array.prototype.map(), but from the type system's perspective there's no meaningful difference between [1,2,3].map and the following definition of foo:

function foo(cb: (x: number) => string) {
    setTimeout(() => cb(100), 1000);
}

a = { cat: "abc", dog: "def" };
if (a) {
    foo(v => a.cat) // error!
    // ----> ~ Object is possibly 'undefined'
}
a = undefined;
// later: Uncaught TypeError: a is undefined

The foo() function takes a callback, and calls it later. And in fact, when it calls it, a is undefined. So the compiler is, in general, right to complain. You could hope that the compiler might notice whether or not a is ever reassigned and suppress the error if it isn't, but that's a lot of extra work for the compiler to do, and the point of microsoft/TypeScript#9998 is that they haven't found anything yet that would pay for itself in terms of compiler performance.

There's a feature request at microsoft/TypeScript#11498 to allow the type system to be told that map() runs its callback immediately, so that any control flow analysis results can be persisted inside the callback, which would allow map() and foo() to be treated differently... but for now it's not part of the language.


So that's the problem. The compiler doesn't know that the callback will be run immediately, and it does not scour the source code to check if a is ever reassigned to undefined, so it errs on the safe side.

Currently the best workaround would be to assign your narrowed value to a new const variable, whose type is by definition already narrowed:

if (a) {
    const _a = a;
    [1, 2, 3].map(v => _a.cat) // okay
}

The _a variable, where it exists, is always of type Animals, not Animals | undefined, so the callback type checks successfully.

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

0

This happens because of how TypeScript handles uncertainty of variable reassignment. Here is the discussion on design choice. If you swap your declaration for a const the error will be cleared in the flow section and mark it at the initialization.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.