Command Palette

Search for a command to run...

Hectal
PHASE 4Beginner ~26 min· topic 1 of 7

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 | B meaning a value that is either an A or a B.
Member
One of the types joined by |. In string | number, string and number are 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, null or undefined.
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.

id.tswhole filets
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.

id.tswhole filets
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.

Array of unions versus union of arraysdiagram
Rendering diagram…

Try it yourself

  1. 1

    Predict the member

    In the first example, change value = "forty-two"; to value = true;. Predict the error text first: which type does TypeScript say true is not assignable to?

    Check the Types panel. It names the whole union, string | number.

  2. 2

    Use a shared method

    In the first example, change id.toUpperCase() to id.toString(). The error disappears, because both members have toString.

    Now try id.toFixed(0). Which member does the second error line blame?

  3. 3

    Widen the union

    In the second example, add a third type type Fish = { name: string; swim(): string }; and change the parameter to pet: Cat | Dog | Fish. Which error lines change?

    The second line names the first member lacking bark. Try adding | undefined too and see what pet.name becomes.

Code & diagrams

A union allows only what every member has

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 function
Unions of objects: only shared properties

pet.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 function
Naming a union, and a union as a function result

toString() 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 00ab

Break 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.

terminal
$ Types panel
── what you'll see ──
index.ts(2,7): error TS2322: Type '(string | number)[]' is not assignable to type 'string[] | number[]'.
Type '(string | number)[]' is not assignable to type 'string[]'.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.

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. --stableTypeOrdering in 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" | string is string), duplicates vanish, and never disappears. 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 with TS2590: 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. 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 | string means "a number or a string". 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. 2

    Unions are everywhere in real code. An id that may be 42 or "A-17". A function that returns a user or null when nobody is found (User | null). An optional setting that is a string or undefined. 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. 3

    What you may do with a union: only what is valid for every member. string | number has toString() and valueOf() because both members do. It does not have toUpperCase(), because numbers don't. TypeScript reports Property 'toUpperCase' does not exist on type 'string | number' and adds a second line telling you which member is the problem. This is the safety: calling toUpperCase() on a number would crash at runtime.

  4. 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. 5

    Unions work with any types, not only primitives (the basic kinds like string, number, boolean). Cat | Dog is a union of two object types. For objects, you may read a property only if every member has it. Cat and Dog both have name, so pet.name is fine. Only Dog has bark, so pet.bark is an error.

  6. 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. 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 covers type in depth). Repeated unions deserve a name, and a good name explains the meaning. A union with one member is just that member, and string | string collapses to string. T | never is T (never is the empty type, Topic 4.6).

Explain it without notes

01

What is a union type and when do you use one?

02

Why can't you call toUpperCase() on a string | number?

03

What is the difference between (string | number)[] and string[] | number[]?

04

How do you get from a union to the specific member you need?

Practice

01

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.

02

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.

03

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 to any in usefulness. Prefer a small, named union that matches a real decision in your program.

Done when you can

  • I can write A | B and 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)[] from string[] | number[].

  • I can name a union with type Name = ....