DEV Community

Cover image for TypeScript Union type a deeper look
Chris Bongers
Chris Bongers

Posted on • Originally published at daily-dev-tips.com

TypeScript Union type a deeper look

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;
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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());
};
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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"];
Enter fullscreen mode Exit fullscreen mode

const Union type

This will result in the union is the same, and we can still get all the values like this:

console.log(Object.values(STATUS));
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
peerreynders profile image
peerreynders

Nitpick - TypeScript Deep Dive: Type Assertion vs. Casting:

The reason why it's not called "type casting" is that casting generally implies some sort of runtime support. However, type assertions are purely a compile time construct and a way for you to provide hints to the compiler on how you want your code to be analyzed.

.

Here we cast an array of possible values as the type of the Status type.

typeof STATUS[number] establishes a type context that refers to the union of all the literal types from the values found in the STATUS array.

It's important to note that you must cast the variable as a const.

It's a const assertion similar to a type assertion.


No Discriminated Unions (alt)?

Collapse
 
lexlohr profile image
Alex Lohr

Nice write-up, especially the low-maintenance patterns.

Collapse
 
dailydevtips1 profile image
Chris Bongers

A lot of thanks to your input in that regard ๐Ÿ™Œ