#Discriminated union with fallback, but still narrowing when discriminator is known

1 messages · Page 1 of 1 (latest)

karmic veldt
#

I have data coming in which every record matches this template:

type G = { kind: string; value: any; };

Where kind is going to be any string out of a finite set, but that set is huge enough, and I need to actually deal with few enough of them, that I don't want to list them all, plus more may be added in future upstream and I don't want my code to break.

I care about specific record kinds, for example these two:

type A = { kind: "a", value: number; }
type B = { kind: "b", value: string; };

Then I have my discriminated union:

type MyRecord = A | B | G;

The real types are more complex; this is just a general idea.

The problem is that when I try to narrow types, for example by having a conditional for x.kind === "a", the G is always still a possibility as far as TS is concerned, and so the type isn't properly narrowed to A. So for x.value I have the type any rather than the number I expect. This is because a record with kind equal to "a" satisfies G as well as A.

Is there a way to type G.kind as "any string except any of the literals elsewhere in this union", and so have the narrowing working as I expect?

I've tried defining G like this instead:

type G = kind: Exclude<string, A["kind"] | B["kind"]>; value: any; }

But that doesn't help -- it still narrows to A | G when I expect it to narrow to A.

I suppose I could just not define the G type at all -- would that be the best way to go? I don't like that I'm then not as type safe, since TS won't complain if I don't handle the case where none of the discriminators matched.

Or I can have a type guard for each of the types I've defined, and use if-elses. That's quite a lot of extra boilerplate though.

thorny jacinth
#

@karmic veldt Yeah, if you have a fallback it kind of stops being a discriminated union, and it becomes somewhat unsafe to try to narrow.

#

If your set isn't exhaustive, TS doesn't really know that you can't also have a { kind: "a", value: SomethingThatIsntNumber }

#

They basically use custom type-guards but with more convenient APIs for working with them.