Union types in TypeScript can be described using the |
(pipe) operator. Also conceptually thought of as the logical boolean operator OR
.
A union of types has the intersection of those types' properties.
With Primitives
TypeScript will only allow an operation on a member of a union if it is valid for every member of the union.
For instance, if the union type is comprised of a number
or a string
then you can only use methods that apply to both types, such as toString
or valueOf
.
You could not call toUpperCase
without first checking that it's a string
, since it could also be a number
.
function printId(id: number | string) {
console.log(id.toUpperCase()); // <- Error
// Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
}
Union of Unions with Primitives
A union of two or more unions (containing primitives) is a combination of all possible types.
type A = number | string;
type B = string | boolean;
type Union = A | B;
// type Union = string | number | boolean
type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';
type Union = A | B;
// type Union = 'a' | 'b' | 'c' | 'd'
Intersection of Unions with Primitives
An intersection of two or more unions (containing primitives) contains the overlap of their types.
type A = number | string;
type B = string | boolean;
type Intersection = A & B;
// type Intersection = string
type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';
type Intersection = A & B;
// type Intersection = 'b' | 'c'
With Objects
Types in the union can have overlap (in the case of objects and tuples), but they don't have to.
The possible overlap would be any similar properties that all objects in the union share.
This is in-line with the first example regarding primitives...only the properties or methods shared between the primitives in the union are available.
Here's an example with objects:
const shared: Error | { name: string; email: string };
shared.name; // <- works b/c both object have a property "name" of type "string"
When a value has a type that includes a union, we are only able to use the “common behavior” that's guaranteed to be there.
That's not always helpful b/c you may in fact need to know EXACTLY which type shared
is. To achieve that, we need to "narrow" or "separate" the potential possibilities for our value.
Type Guards and Discriminated Unions
function flipCoin(): 'heads' | 'tails' {
if (Math.random() > 0.5) return 'heads';
return 'tails';
}
function maybeGetUserInfo():
| ['error', Error]
| ['success', { name: string; email: string }] {
if (flipCoin() === 'heads') {
return ['success', { name: 'Nate Stephens', email: 'nate@example.com' }];
} else {
return ['error', new Error('The coin landed on TAILS :(')];
}
}
const outcome = maybeGetUserInfo();
// TYPE of "outcome"
// -----------------
// const outcome:
// | ['error', Error]
// | ['success', { name: string; email: string; } ];
The destructured values from outcome
could be one of two types when looked at separately:
const outcome = maybeGetUserInfo();
const [first, second] = outcome;
// first: "error" | "success"
//
// second: Error | {
// name: string;
// email: string;
// }
Narrow with a type guard
Type guards are expressions which, when used with control flow statements, allow us to identify a more specific type for a particular value.
We could run a check on second
and verify if it is either an instance of Error
or an object
.
But a simpler approach is using the string value of first
to determine the value of second
in outcome
.
Discriminated Unions (aka "Tagged" Union)
Use a property in an object or a value in a tuple as a "key" to run a type guard.
TypeScript understands that the first and second positions of our tuple are linked.
Therefore, if we can successfully identify the first part of the tuple we'll know what the second part of it is.
const outcome = maybeGetUserInfo();
if (outcome[0] === 'error') {
// In this branch of your code, second is an Error
outcome;
// const outcome: ["error", Error]
} else {
// In this branch of your code, second is the user info
outcome;
// const outcome: ["success", {
// name: string;
// email: string;
// }]
}
From the TypeScript Fundamentals, v3↗ course on FEM↗ taught by Mike North↗.
From the React and TypeScript, v2↗ course on FEM↗ taught by Steve Kinney↗.
From the TypeScript↗ docs.