# Union Types and Narrowing

> Source: https://learn-typescript.org/union-types-and-narrowing/
> Part of Learn TypeScript, free to read.

A union type says a value is one of several possibilities:

```typescript
type Id = string | number;
type Status = "pending" | "shipped" | "cancelled";

let id: Id = "abc";
id = 42;          // also fine
id = true;        // error
```

Those quoted strings are **literal types** — a type whose only value is that exact string. A union of them is how you model a fixed set of options, and it is better than an enum for most purposes because it emits no runtime code.

```typescript
function setStatus(s: Status) {}
setStatus("shipped");     // fine
setStatus("shiped");      // error, with a spelling suggestion
```

## Narrowing

You cannot use a union until you know which member you have:

```typescript
function format(id: string | number): string {
  return id.toUpperCase();      // error — number has no toUpperCase
}
```

**Narrowing** is proving to the compiler which member it is. Ordinary JavaScript checks do it:

```typescript
function format(id: string | number): string {
  if (typeof id === "string") {
    return id.toUpperCase();    // here, id is string
  }
  return id.toFixed(2);         // here, id is number
}
```

That is the part people find magical at first: the compiler follows your control flow. Inside the `if`, `id` genuinely has type `string`.

### The narrowing tools

```typescript
typeof x === "string"          // primitives
Array.isArray(x)               // arrays
x instanceof Error             // classes
"email" in user                // presence of a property
x === null / x !== undefined   // equality
if (x) { }                     // truthiness — careful with 0 and ""
```

:::warn Truthiness narrowing has a trap
```typescript
function render(count: number | undefined) {
  if (!count) return "none";     // also catches 0
  return `${count} items`;
}
```
`0` is falsy, so a real count of zero takes the `undefined` path. Check explicitly — `if (count === undefined)` — whenever `0` or `""` are valid values. This is one of the most common bugs in generated TypeScript.
:::

## Discriminated unions

The pattern that makes unions genuinely powerful. Give each member a shared literal property, and narrowing on that property picks the whole shape.

```typescript
type Result =
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: Error };

function render(r: Result): string {
  switch (r.status) {
    case "loading": return "Loading…";
    case "success": return r.data.email;     // data exists only here
    case "error":   return r.error.message;  // error exists only here
  }
}
```

Compare that with the shape generated code usually produces:

```typescript
// 16 possible states, most of them nonsense
type Result = {
  status: "loading" | "success" | "error";
  data?: User;
  error?: Error;
};
```

With the second version you must check `r.data` everywhere and nothing stops a `"success"` with no data. With the first, **the invalid states cannot be constructed**. That is the single most valuable modelling habit in TypeScript.

## Exhaustiveness checking

Add a `default` branch that assigns to `never`, and adding a new union member becomes a compile error everywhere it needs handling:

```typescript
function assertNever(x: never): never {
  throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}

function render(r: Result): string {
  switch (r.status) {
    case "loading": return "Loading…";
    case "success": return r.data.email;
    case "error":   return r.error.message;
    default:        return assertNever(r);
  }
}
```

Now add `| { status: "cancelled" }` to `Result`. The `default` branch fails to compile, because `r` is no longer `never` there — and it fails in *every* switch that needs updating. The compiler has just found all your work for you.

This is the pattern that makes a large TypeScript codebase safe to change.

## Type predicates

When a check is too complex for the built-in narrowing, write a function that returns a type predicate:

```typescript
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" && value !== null &&
    "id" in value && "email" in value
  );
}

const data: unknown = JSON.parse(raw);
if (isUser(data)) {
  data.email;      // narrowed to User
}
```

`value is User` tells the compiler that a `true` return means the narrowing holds.

:::warn A type predicate is a promise you make
The compiler trusts it. If your check is wrong, the type is wrong and nothing will tell you. For data crossing a real boundary — a network response, a database row — prefer a schema library that generates both the check and the type from one declaration, rather than hand-writing predicates.
:::

## Exercise

```typescript
// Model the outcome of a payment as a discriminated union:
//   - "approved" with a transactionId (string)
//   - "declined" with a reason (string)
//   - "pending" with a retryAfter (number of seconds)
// Write `describe(result)` returning a human sentence for each,
// with an assertNever default branch.

// write your code here
```

## Common questions

### Union of literals or an enum?

A union of string literals, in most cases. It emits no runtime JavaScript, it narrows naturally in a switch, and the values are just strings so they serialise cleanly. Enums generate real code and are not erasable syntax, which matters if you use a runtime type-stripper.

### Why does my narrowing stop working inside a callback?

TypeScript cannot guarantee a narrowing still holds inside a function that may run later, because the variable could have been reassigned in between. Assign the narrowed value to a `const` first and use that inside the callback.

### When should I write a type predicate rather than parse?

Predicates suit checks over data you already own — distinguishing two internal shapes. For anything arriving from outside your program, parse with a schema instead: a predicate asserts a belief, whereas a parse actually verifies it and gives you the type as a by-product.
