The TypeScript Union type is excellent if your type can consist of multiple values/types.
We define a union type using the pipe character (|
).
For instance this Union type between a string and a number.
type myUnion = string | number;
However, depending on what we want to do with this type, it can be difficult.
For one, the Union type can only perform valid actions for both types.
Let's see how that would in an example:
type myUnion = string | number;
const myUnionFunction = (property: myUnion) => {
console.log(property);
};
myUnionFunction(123);
myUnionFunction('abc');
This will both be valid since a console log is valid for both, but what if we want to introduce a manipulation on the string only?
const myUnionFunction = (property: myUnion) => {
console.log(property.toUpperCase());
};
This will quickly throw an error because we can't convert the 123
value to uppercase.
In this case, we can use narrowing to narrow down what action to perform for which type.
type myUnion = string | number;
const myUnionFunction = (property: myUnion) => {
if (typeof property === 'string') {
property = property.toUpperCase();
}
console.log(property);
};
myUnionFunction(123);
myUnionFunction('abc');
And in the above example, we neatly get ABC
returned, while the numeric value has not changed.
Other use-cases of Unions
Now that we have seen the default string
or number
value, let's look at other use-cases for the union type.
For one, we could define different user states.
type IsUser = User | LoggedUser;
This will distinguish between a user or logged user type, and such comparisons can be super handy if you are only using a subset of both types.
Another great example is to catch certain events like this:
type Event = MouseEvent | KeyboardEvent;
And a super powerful one is a string union type, which can act very close to the enums we saw.
type Status = 'not_started' | 'progress' | 'completed' | 'failed';
We have a function that can set this status, and we want to make sure it only accepts those values.
type Status = 'not_started' | 'progress' | 'completed' | 'failed';
const setStatus = (status: Status) => {
db.object.setStatus(status);
};
setStatus('progress');
setStatus('offline');
The bottom line will throw an error stating it can't set the value to offline
as it does not exist in this union type.
Union type limitations
A union type is only available at compile-time, meaning we can't loop over the values.
LEt's say we need the array of all possible status values we just defined.
Normally we would try something like this:
console.log(Object.values(Status));
This will throw an error stating we can't use Status
as a value since it only exists as a type.
This is something we could achieve with an enum.
enum Status {
'not_started',
'progress',
'completed',
'failed'
}
console.log(Object.values(Status));
However, there is another way to do this, which might even make more sense to use:
const STATUS = ["not_started", "progress", "completed", "failed"] as const;
type Status = typeof STATUS[number];
Here we cast an array of possible values as the type of the Status
type.
It's important to note that you must cast the variable as a const
. You can either use the above method or the following one:
const STATUS = <const>["not_started", "progress", "completed", "failed"];
This will result in the union is the same, and we can still get all the values like this:
console.log(Object.values(STATUS));
All these little gimmicks make Typescript such a bliss to work with.
The possibilities are endless.
Thank you for reading, and let's connect!
Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter
Top comments (3)
Nitpick - TypeScript Deep Dive: Type Assertion vs. Casting:
.
typeof STATUS[number]
establishes a type context that refers to the union of all the literal types from the values found in theSTATUS
array.It's a const assertion similar to a type assertion.
No Discriminated Unions (alt)?
Nice write-up, especially the low-maintenance patterns.
A lot of thanks to your input in that regard π