Discriminated Unions or Tagged Unions Types - TypeScript Narrowing #4
TypeScript Narrowing #4
Heeeello and welcome to the fourth article in our TypeScript narrowing series. We're continuing from where we left off, so If you've missed any of the previous articles, I suggest you go back and read them.
In this article, we will cover tagged union types (AKA discriminated unions).
And we have more to come, in future articles:
- Assertion guards
- Higher-order guards
- Narrowing library
I'm Lucas Paganini, and on this site, we release web development tutorials.
Tagged Union Types (Discriminated Unions)
So, tagged union types... what are they?
It's pretty self-explanatory. They are union types with a tag π
For example, you could have a union type called Log
that aggregates three interfaces: Warning
, Debug
, and Information
.
type Log = Warning | Debug | Information;
interface Warning {
text: string;
}
interface Debug {
message: string;
}
interface Information {
msg: string;
}
TypeScripttype Log = Warning | Debug | Information;
interface Warning {
text: string;
}
interface Debug {
message: string;
}
interface Information {
msg: string;
}
Cool, now we have a union type. To turn that into a tagged union type, we need a common property with literal types between our interfaces. It could be any property name, but to simulate a real-world example, we'll call it .subscribeToTheNewsletter
.
This property will serve as an ID for the different types of interfaces that make up the Log
type. Every interface will have a different literal type for that property.
type Log = Warning | Debug | Information;
interface Warning {
subscribeToTheNewsletter: 'like';
text: string;
}
interface Debug {
subscribeToTheNewsletter: 'comment';
message: string;
}
interface Information {
subscribeToTheNewsletter: 'share';
msg: string;
}
TypeScripttype Log = Warning | Debug | Information;
interface Warning {
subscribeToTheNewsletter: 'like';
text: string;
}
interface Debug {
subscribeToTheNewsletter: 'comment';
message: string;
}
interface Information {
subscribeToTheNewsletter: 'share';
msg: string;
}
And that's it. The .subscribeToTheNewsletter
property is serving as a tag for the Log
type, so it is now a tagged union type.
Discriminated Union Terminology
As I've mentioned before, this is also known as a discriminated union, and our .subscribeToTheNewsletter
property is the discriminant property of Log
.
To quote from the TypeScript docs: "When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union."
From now on, I'll use the "discriminated unions" terminology, instead of "tagged union types" because that's how TypeScript is calling it nowadays, and I want to be consistent with the official terminologies β even though I believe "tagged union types" would be a better name for it.
Type Guards for Discriminated Unions
"Ok Lucas, I got it. But what's the point? What do I get to do with a discriminated union?"
Good question.
What you get with a discriminated union is a type guard for all the possible discriminations of the union type.
For example, if you have a variable called value
that is a Log
. That means that value
could be either a Warning
, a Debug
or an Information
.
let value: Log;
if (isWarning(value)) {
// Handle the Warning case
} else if (isDebug(value)) {
// Handle the Debug case
} else if (isInformation(value)) {
// Handle the Information case
}
const isWarning =
(value: unknown): value is Warning => { ... }
const isDebug =
(value: unknown): value is Debug => { ... }
const isInformation =
(value: unknown): value is Information => { ... }
TypeScriptlet value: Log;
if (isWarning(value)) {
// Handle the Warning case
} else if (isDebug(value)) {
// Handle the Debug case
} else if (isInformation(value)) {
// Handle the Information case
}
const isWarning =
(value: unknown): value is Warning => { ... }
const isDebug =
(value: unknown): value is Debug => { ... }
const isInformation =
(value: unknown): value is Information => { ... }
But instead of creating individual guards for those three interfaces, you could just check the value of the discriminant property.
If .subscribeToTheNewsletter
is "like"
, you know that it's a Warning
. If it's "comment"
, it's a Debug
. And if it's "share"
, it's an Information
.
let value: Log;
if (value.subscribeToTheNewsletter === 'like') {
// Handle the Warning case
} else if (value.subscribeToTheNewsletter === 'comment') {
// Handle the Debug case
} else if (value.subscribeToTheNewsletter === 'share') {
// Handle the Information case
}
TypeScriptlet value: Log;
if (value.subscribeToTheNewsletter === 'like') {
// Handle the Warning case
} else if (value.subscribeToTheNewsletter === 'comment') {
// Handle the Debug case
} else if (value.subscribeToTheNewsletter === 'share') {
// Handle the Information case
}
And you can make your code even clearer by going an extra mile and switching your if statements β got it? π "switching"... "switch"... no? ok...
let value: Log;
switch (value.subscribeToTheNewsletter) {
case 'like':
// Handle the Warning case
case 'comment':
// Handle the Debug case
case 'share':
// Handle the Information case
}
TypeScriptlet value: Log;
switch (value.subscribeToTheNewsletter) {
case 'like':
// Handle the Warning case
case 'comment':
// Handle the Debug case
case 'share':
// Handle the Information case
}
Conclusion
References and links for the previous articles are in the references.
If you enjoyed the content, you know what to do.
And if your company is looking for remote web developers, consider contacting me and my team. You can do so on lucaspaganini.com.
Have a great day, and I'll see you soon.
Related content
- TypeScript Narrowing Part 1 - What is a Type Guard
- TypeScript Narrowing Part 2 - Fundamental Type Guards
- TypeScript Narrowing Part 3 - Custom Type Guards