#Can I use an enum as a discriminator in a discriminated union?
59 messages · Page 1 of 1 (latest)
It's perfectly fine to use enum values as the discriminant
are you sure? or, well, I"m trying to do this
enum Disc {
A
B
}
type MyTypeA {
discriminator: "A"
...
}
type MyTypeB {
discriminator: "B"
...
}
and that doesn't seem to work but I can't do
discriminator: Disc.A
Works fine on the playground
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
But the value of Disc.A is not "A"
It is the number 0
And Disc.B is 1
If you want the values to be those strings, then set it explicitly
enum Disc {
A = "A",
B = "B",
}
omg it does work. It's one of those time where you are making a bunch of changes trying to find the solution and you don't isolate each change sufficiently. Sorry. Thanks for clearing it up for me.
@modest relic Hi not sure if you're till around. I'm having a trouble with these distinguishers again. Pretty sure I'm not doing it right.
I have my type definition in which I'm doing {myDist: "A"} and then I'm explicitly setting the value for myDist to "A" on the concrete object as well. but when I try to say if(X.myDist === "A") it just doesn't narrow
@heady siren Can you make a reproduction?
enums are opaque, if you have an enum type you'd need Disc.A, not "A"
yeah, and on top of that ☝️ if you don't want to refer to the enum itself in type definitions or runtime code then there's no benefit to having it (just use a union of "A" | "B")
Hi, thanks for the responses.
So a) you say "if you don't want to refer to the enum in the type def" I don't mind, I just don't know how, i.e. don't know the syntax. b) I kind of have to use the enum because we have the enum for other purposes and I don't want to refer to the same list of values in different ways.
For now I've had to resort to using in "'somekeyinthedistinctType' in generalType" to get narrowing
that kinda defeats the purpose of using a DU then
I agree.
Aswin already demonstrated how, but here's a more complete example:
Preview:```ts
enum Disc {
A,
B,
}
type MyTypeA = {
discriminator: Disc.A
a: string
}
type MyTypeB = {
discriminator: Disc.B
b: number
}
declare const aOrB: MyTypeA | MyTypeB
if (aOrB.discriminator === Disc.A) {
aOrB.a
// ^?
} else {
aOrB.b
// ^?
...```
regarding b: generally, no, you never have to use an enum
it's directly replaceable with an object with the same keying/access functionality, or an array for iteration, or just nothing (and using the string union directly) if you don't need either
So, when I did 'discriminator: Disc.A' I got an error in the ide. Which makes me think, maybe it was an "error" like a lint error not an "I wont compile for you error" which is very frustrating because that's a whole different story. I"ll have to try again and see
Well, I mean I have to in the sense that in the code base there is a list of values. I can reprodue that list of values in another way e.g. an enum and a string union, but then I have to keep the two insync.
in most cases you won't need 2 separate lists that you would need to keep synced
you can have a single source of truth then derive the other from that
true. just sayin, I got one may as well. use it. Thank you for the help. I'm going to try this out and I'll get back with the results in a little bit.
@spring root Hi, so here is what's happening. When I define my types they inherit some base properties from a basetype. that base type has the distinguisher on it, however the distinguisher is optional because the base type is used else where too. So I define my type like this
export type FinancialIncident = Omit<Incident, 'vertical'> & {
vertical: 'FINANCIAL';
...
then in the code I do if (incident.vertical === 'FINANCIAL') { and I try to use the incident as a FinancialIncident. but I get this error
The intersection 'Omit & { vertical: "FINANCIAL"; parties: FinancialParty[]; directorOrOfficerType?: DirectorOrOfficerType | undefined; financialIncidentId?: string | undefined; } & { ...; } & { ...; } was reduced to 'never because property vertical has conflicting types in some constituents.
right, that would be because you're trying to narrow Incident to FinancialIncident via vertical, which are unrelated in those 2 types
how does Incident define vertical?
well, so Incident is a base type and it has vertical defined, basically like vertical?: 'FINANCIAL'|'LIABILITY'|...etc; But then as above FinancialIncident has vertical defined as 'FINANCIAL'
ok, so just don't use Omit
type Wide = { a?: "a" | "b", b: number, c: string }
// ^? - type Wide = {
// a?: "a" | "b";
// b: number;
// c: string;
// }
type Narrow = Wide & { a: "a", b: 1 | 2 }
// ^? - type Narrow = Wide & {
// a: "a";
// b: 1 | 2;
// }
this would also let you catch the issue quicker if you mistype the value, even more so in an interface
interface Parent { a: "a" | "b" }
type WrongType = Parent & { a: "c" }
type W_ = WrongType["a"]
// ^? - type W_ = never
interface WrongI extends Parent {
// ^^^^^^
// Interface 'WrongI' incorrectly extends interface 'Parent'.
// Types of property 'a' are incompatible.
// Type '"c"' is not assignable to type '"a" | "b"'.
a: "c"
}
well, so I need to omit somethings. so what I just tried was removing 'vertical' from omit but the error persists
that's likely not the case
you don't need to Omit anything, you just need to set up the base types correctly, to say that any of the child types is also a valid base type
that's the point of base types
using Omit obscures that purpose, making issues harder to track
you have something like { vertical: "a" } & { vertical: "b" } somewhere
the solution is to fix the mistake in the definition, not to Omit more stuff
it may be a typo somewhere
interesting. I might need some help with that. Basically I have a short graph
Case
- Incident
- Party[]
- Contact[]
- Party[]
where all those are base types. Then I have
FinancialCase
- FinancialIncident
- FinancialParty[]
- FinancialContact[]
- FinancialParty[]
where the F types have just a couple properties in them. So what I do is this
export type CyberIncident = Omit<Incident, 'parties'> & {
vertical: 'CYBER';
parties: CyberParty[];
};
In order to make sure the CyberIncident has a collection of CyberParty(s) rather than just party(s)
if CyberParty is a subtype of Party then you don't need that Omit at all
really? that would certainly simplfy things. It's hard to know when I can't get it to narrow properly, but solving one may solve the other
well, how's CyberParty defined?
or perhaps more straightforward, is CyberParty just a more specific Party?
if so, that's what subtypes are
Yes, so it's defined just the same way as CyberIncident above. Although I left out the extra properties so its more like this
export type CyberIncident = Omit<Incident, 'parties'> & {
vertical: 'CYBER';
parties: CyberParty[];
cyperProperty1: stirng
cyberProperty2:
};
so yeah if you're just specifying subtypes then you don't need Omits at all
That is awesome. as per usual I am over complicating things. I"ll give it a try.
thanks!
So should I still "add" the subtypes? e.g. if Incident has parties: Party[] In cyberIncident I don't omit "parties" but should I add a property parties: CyberParty[] ?
@spring root Hello, I wonder if we could spend a minute on this thing again. I'm afraid it's due eod on monday so I may not get there, but it's driving me crazy
What I have is a graph of base types. Now those basetypes are ... not very simple but ultimately I know what is on them. Then I have my subtypes. These subtypes no longer omit anything and they are simple maybe just a couple new properties and the "vertical" value but when I go to try to use it I get
incident.vertical === 'CYBER' && incident.cyberCrime "cyberCrime doesn't exist on incident"
one thing is that the base type does have vertical: CYBER"| "FINANCIAL"| etc on it and I'm not overriding that or omiting it at all. except in the subtypes where I am overriding it
@heady siren can you share a simplified example of what you have now?
the type of incident should look like type Incident = SpecificIncident1 | SpecificIncident2 | … if you want to be able to discriminate it
Hey, sorry I didn't work up an example. I really fried and still have work to do. But I see you comment here. The issue is that Incident is a base type that SpecificIncident1 etc extend. So would it be
type Incident = SpecificIncident1 | SpecificIncident2 | {whole bunch of stuff for the baes type} ?
it's hard to be sure without knowing exactly what you're doing, but it sounds like you need two separate things:
- a set of common properties that you don't want to write over and over again (the "base type")
- the type of
incident, which is specifically either aCyberIncidentor aWhateverOtherKindOfIncident
does that sound right?
if so, you'd define the common supertype stuff as an object type in BaseIncident, and type incident as a union like i mentioned above. CyberIncident would intersect with BaseIncident, not that union type