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
amight beundefined, nota.cat. I'm going to edit so that this is actually a minimal reproducible exampleashould be inferred as beingAnimalswithin the scope of the if statement. Assigninga.catto 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