Topic 4.1
Union Types
In one line
A union type, written A | B, says a value is one of several types. TypeScript only lets you do what is safe for every member, until you check which one you really have.
Think of it like this
A parcel locker accepts a small box or a large box. When you walk up to it you don't know which size is inside. You can ask "how heavy is it?" because every parcel has a weight. You can't ask "which flap do I open?" until you look at the box. A union is a value that is one of several kinds, and until you check, you may only use what all the kinds share.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Union type
- A type written
A | Bmeaning a value that is either anAor aB. - Member
- One of the types joined by
|. Instring | number,stringandnumberare the members. - Narrowing
- Checking which member of a union you hold, so TypeScript lets you use that member's specific features.
- Type alias
- A new name for a type, made with
type Name = .... It does not create a new type, only a label. - Primitive
- A basic value kind like
string,number,boolean,nullorundefined. - Common member
- A property or method that every member of the union has. Only those are allowed on an un-narrowed union.
- Mixed array
- An array whose items can be of different kinds, typed
(string | number)[]. - Assignable
- A value of one type is allowed where another type is expected. Error messages say "not assignable" when the value does not fit.
Step by step
01Accept two kinds of input
A function that takes an id. Some systems number their items, others use codes like "A-17". The union lets the parameter accept both.
Write id: number | string. A call with 7 or with "A-17" is fine, and a call with true is an error.
function find(id: number | string) {
console.log("looking for", id);
}
find(7);
find("A-17");
find(true); // error TS2345: Argument of type 'boolean' is not assignable to parameter of type 'string | number'.02Only the shared features are allowed
Inside the function, id could be either. Both numbers and strings have toString(), so that works. Only strings have toUpperCase(), so TypeScript refuses.
The error has two lines. The first says toUpperCase is missing on string | number. The second says which member lacks it: number. Read the second line first: it names the culprit.
03Check, then use
To use a string-only method, first prove the value is a string. typeof id === "string" is that proof. Inside the if, TypeScript treats id as string.
Topic 4.3 explains exactly how TypeScript follows this. For now, the pattern is: union in, check, then use.
function shout(id: number | string) {
if (typeof id === "string") {
return id.toUpperCase(); // here id is a string
}
return id.toFixed(0); // here id is a number
}04Unions of objects: read only the common properties
A pet is a Cat or a Dog. Both have name, so pet.name is allowed. Only dogs can bark(), so pet.bark() is an error until you narrow.
This is the same rule as for primitives, seen on objects: the union has the properties that all members share.
05Array of unions versus union of arrays
(string | number)[] is one array with mixed items. string[] | number[] is one of two array kinds. They are different promises.
When you write an array literal with different kinds of items, TypeScript infers the mixed form. The picture below shows the two shapes.
Try it yourself
- 1
Predict the member
In the first example, change
value = "forty-two";tovalue = true;. Predict the error text first: which type does TypeScript saytrueis not assignable to?Check the Types panel. It names the whole union,
string | number. - 2
Use a shared method
In the first example, change
id.toUpperCase()toid.toString(). The error disappears, because both members havetoString.Now try
id.toFixed(0). Which member does the second error line blame? - 3
Widen the union
In the second example, add a third type
type Fish = { name: string; swim(): string };and change the parameter topet: Cat | Dog | Fish. Which error lines change?The second line names the first member lacking
bark. Try adding| undefinedtoo and see whatpet.namebecomes.
Code & diagrams
The error shows two lines: the first names the union, the second names the member that lacks the method. The code still runs, and describe(7) crashes at runtime. That crash is what the error predicted. Note that TypeScript prints the union as string | number even though we wrote number | string.
Console
(nothing was printed)Expected console output
Uncaught TypeError: id.toUpperCase is not a functionpet.name is fine because both types have it. pet.bark is not, so the error names Cat as the member without it. The array literal is inferred as a mixed array. The last line sends a Cat, so the crash happens exactly where the error said.
Console
(nothing was printed)Expected console output
Tom
Uncaught TypeError: pet.bark is not a functiontoString() is shared by numbers and strings, so pad accepts an Id. The last line mixes the properties of the two Shape members: TypeScript checks the object against the member picked by kind, and side does not belong to a circle. Topic 4.5 builds on this exact shape.
Console
(nothing was printed)Expected console output
0007 00abBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Array of strings or array of numbers
Declare const both: string[] | number[] = ["a", 1]; next to a (string | number)[] version.
Myth vs fact
Myth
A union lets you call any method of any member.
Fact
Only what every member has. To use a member's own features you must narrow first.
Myth
string | number and number | string are different types.
Fact
The order doesn't matter. TypeScript treats them as the same type and may print the members in a different order than you wrote.
Myth
A mixed array and an array union mean the same thing.
Fact
(string | number)[] allows mixed items. string[] | number[] is one array of a single kind.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Internally a union is stored as a sorted list of its members, ordered by an internal type id (the order types were created in the checker). That is why the printed order can differ from what you wrote, and in TypeScript 6.0 it depends on which types the checker happened to create first.
--stableTypeOrderingin 6.0, and always on in 7.0, makes the order deterministic. Never rely on the printed order of a union. - ▸
TypeScript reduces unions. Literal members are absorbed by their wider type (
"a" | stringisstring), duplicates vanish, andneverdisappears. Large unions have costs: assignability and narrowing may compare many pairs, and an expression that would produce a union of more than about 100,000 members is rejected withTS2590: Expression produces a union type that is too complex to represent(a template literal type built from three unions of 50 members each, 125,000 combinations, triggers it). Template literal types can create such unions by accident (Topic 9.4). - ▸
Object unions are checked member by member, which is why excess-property checks on a literal use the member picked by the tag (see the third example). For unions without a tag, TypeScript allows a property that exists in any member for an object literal, so the check is lenient there.
Remember this
- 1
The problem this solves: Real values are often one of several kinds, such as an ID that is a number or a text. A single type forces you to pick one and reject the other. A union type is written with a vertical bar:
number | stringmeans "anumberor astring". Each part is a member of the union. The variable holds exactly one of the members at any moment. It is the type-level word "or". - 2
Unions are everywhere in real code. An id that may be
42or"A-17". A function that returns a user ornullwhen nobody is found (User | null). An optional setting that is astringorundefined. A value read from a form that is text now and a number after parsing. Whenever the honest answer to "what is it?" is "it depends", you want a union. - 3
What you may do with a union: only what is valid for every member.
string | numberhastoString()andvalueOf()because both members do. It does not havetoUpperCase(), because numbers don't. TypeScript reportsProperty 'toUpperCase' does not exist on type 'string | number'and adds a second line telling you which member is the problem. This is the safety: callingtoUpperCase()on a number would crash at runtime. - 4
To use something that only some members have, you first narrow the union: check which member you hold, and TypeScript treats the value as that member inside the check. Narrowing is the rest of this phase (Topics 4.3 to 4.7). Here, just remember the shape of the idea: a union gives you freedom to accept several things, and narrowing is how you win back the specific powers.
- 5
Unions work with any types, not only primitives (the basic kinds like
string,number,boolean).Cat | Dogis a union of two object types. For objects, you may read a property only if every member has it.CatandDogboth havename, sopet.nameis fine. OnlyDoghasbark, sopet.barkis an error. - 6
Two forms are easy to confuse.
(string | number)[]is one array whose items are each a string or a number (a mixed array).string[] | number[]is either an array of only strings or an array of only numbers, decided for the whole array. The mixed array["a", 1]fits the first and is rejected by the second. TypeScript infers(string | number)[]for a mixed array literal. - 7
You can name a union with a type alias:
type Id = number | string;. An alias is a new name for an existing type, nothing more (Topic 3.1 coverstypein depth). Repeated unions deserve a name, and a good name explains the meaning. A union with one member is just that member, andstring | stringcollapses tostring.T | neverisT(neveris the empty type, Topic 4.6).
Explain it without notes
What is a union type and when do you use one?
Why can't you call toUpperCase() on a string | number?
What is the difference between (string | number)[] and string[] | number[]?
How do you get from a union to the specific member you need?
Practice
Write a function double that takes a number | string. For a number it returns the number times two, for a string it returns the string repeated twice. Use a check so both branches type-check.
Declare a type alias Pet for Cat | Dog with a name shared by both. Write function nameOf(pet: Pet) that returns the name without any check, and explain why that works.
Declare const items = [1, "a", true]; and write down its inferred type. Then declare the same kind of array but forbid booleans.
Trade-offs
- ↔
Unions are honest: they say the value can vary. The price is that each use must be preceded by a check. If you find yourself writing the same check everywhere, model the data better (Topic 4.5) or check once at the edge.
- ↔
A very wide union (
string | number | boolean | null | undefined | object) is close toanyin usefulness. Prefer a small, named union that matches a real decision in your program.
Done when you can
I can write
A | Band read it as "A or B".I can explain why only the shared members of a union are allowed.
I can read the two-line error for a missing property on a union.
I can tell
(string | number)[]fromstring[] | number[].I can name a union with
type Name = ....