1

I'm trying to put together some data, that's an array of union types, and process it accrodingly. I don't understand why typescript can't narrow this union types down:

interface BaseEvent {
    id: number;
}

interface EventA extends BaseEvent {
    attrA: string;
}

interface EventB extends BaseEvent {
    attrB: string;
}

type Events = (EventA | EventB)[];

const events: Events = [{ id: 1, attrA: "A" }, { id: 2, attrB: "B" }]

events.forEach(event => { // type of event is already: (parameter) event: EventA | EventB
    if (typeof event === "EventB") { // <- it doesn't work 

        const eventId = event.id // can only infer id here
    }
}) 

I've tried to use intersections suggested by this answer, but it does not seems to work either. What's the right approach here?

TS playground

Enfield Li
  • 1,952
  • 1
  • 8
  • 23
  • the answer below should solve your problem. `typeof` includes just the js values listed here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof. You may use `instanceof` for more specific typing. – okzoomer Oct 20 '22 at 04:17
  • https://www.typescriptlang.org/docs/handbook/advanced-types.html this explains that typeof X === 'xyz' that is _not_ of the default javascript typeof return types is _not_ recognized as a type guard. So to allow type narrowing, you'd need to use a different method. Perhaps a discriminated union (a shared field has differing values that differentiates its type) or a unique attribute. – Yuji 'Tomita' Tomita Oct 20 '22 at 04:18
  • Thank you so much for the addtional resource! I'll look it up. – Enfield Li Oct 20 '22 at 04:20

1 Answers1

2

Since your events have differing properties, you can use in:

if ("attrA" in event) { // event is now EventA

Playground

kelsny
  • 23,009
  • 3
  • 19
  • 48
  • Ah, I totally forgot `in` keyword is a thing in JS, I'll accept after the timmer's up. – Enfield Li Oct 20 '22 at 04:19
  • @Enfieldli yeah `in` is a "standard js thing" but the key is that TS also uses "in" as type guards. It's weird what TS will infer as a type guard and what not. Like until recently, a constant condition defined outside the if statement would _not_ be a type guard. – Yuji 'Tomita' Tomita Oct 20 '22 at 04:25
  • @Yuji 'Tomita' Tomita Sometimes it just works without me knowing how :-p, can you link to that change you memtioned, I want to look more into it : ) – Enfield Li Oct 20 '22 at 04:29
  • @Enfieldli took me a while to find this; typescript is only learned through github issues lol: https://github.com/microsoft/TypeScript/issues/12184 `Control flow analysis of aliased conditional expressions and discriminants #44730` -- aliased conditional expressions i.e. `const myCondition = foo.bar === 123; if (myCondition)` would not have been a type guard until this patch – Yuji 'Tomita' Tomita Oct 20 '22 at 04:47
  • @Enfieldli and for the record, I think it's "most of the time, it works without us knowing how". It's what trips up so many people myself included about TS until we understand, it's not as deterministic as a typical programming language. It _infers_ things and sometimes can, sometimes can't, infer typings, narrowings, etc. and we hope it does it most of the time. – Yuji 'Tomita' Tomita Oct 20 '22 at 04:50
  • @Yuji 'Tomita' Tomita Wow, that's really refreshing! I wasn't aware we can narrow types this way, and it does provide me a second approach for the problem, and interestingly, just looking at the docs does __not__ make much sense before, not until I'm actually facing the problem... Guess I've taken too many things for granted from TS . Anyways, thank you so much for the enlightenment! – Enfield Li Oct 20 '22 at 05:11