Types and narrowing — TypeScript · Foundations

Describe data with types precise enough to rule out wrong values, then turn a value of unknown shape, such as a request body, into one TypeScript trusts, one check at a time.

What you will learn

Read the theory for Types and narrowing

All TypeScript lessons

All Foundations courses

loading types…

What you'll learn

  • Write union and literal types that allow exactly the values that make sense
  • Narrow unknown with typeof, equality, Array.isArray and in, and write a type predicate
  • Model a result that is one of two shapes as a discriminated union, and narrow on its tag

Types and narrowing

A server spends its life receiving values it did not create: a request body, a query string, a row from a database, a message from another service. In JavaScript those arrive as whatever they happen to be, and the first sign that age was the string "3" is a bug three functions later. TypeScript lets you say what a value must be, and then holds every line of code to it. This lesson is about the two halves of that: types precise enough to rule out wrong values, and the checks that turn an unknown value into one TypeScript trusts.

Types describe values

An annotation says what a variable, parameter or return value holds. Where TypeScript can see the value, it infers the type, so annotations belong on the edges: parameters, return types, exported data.

type Owner = { name: string; email: string; plan: 'free' | 'pro' };

'free' | 'pro' is a union of two literal types: not "any string", but exactly one of those two. plan: 'enterprise' is now a compile error, caught before the code runs. The narrower the type, the more mistakes the compiler finds for you.

unknown, not any

any switches the checker off: you can call anything on it, and nothing is checked. unknown is its honest twin: it can hold any value, and you can do nothing with it until you have checked what it is. A request body is unknown. Treating it as any is how body.age + 1 becomes "31".

Narrowing

Inside an if that tests a value, TypeScript knows the test passed and narrows the type:

CheckWhat TypeScript learns
typeof x === 'string'x is a string (also 'number', 'boolean', 'object', 'function')
x === null, x !== undefinedthe value is, or is not, that one
Array.isArray(x)x is an array
'email' in xx has that property

Two traps. typeof null is 'object', so an object check needs x !== null too; and arrays are objects, so "a plain object" also needs !Array.isArray(x). Narrowing also works by elimination: after if (typeof x !== 'string') return;, the rest of the function knows x is a string.

function planOf(input: unknown): 'free' | 'pro' {
  if (typeof input !== 'object' || input === null) return 'free';
  const { plan } = input as Record<string, unknown>;
  return plan === 'pro' ? 'pro' : 'free';
}

as Record<string, unknown> says "an object whose fields are all unknown", which is true after the checks above, and still makes you check each field.

Type predicates

A check you need in several places belongs in a function, but a function returning boolean teaches the caller nothing. A type predicate as the return type does:

function isPlan(value: unknown): value is 'free' | 'pro' {
  return value === 'free' || value === 'pro';
}

After if (isPlan(x)), x has that type. The compiler trusts your predicate, so it has to be right.

Discriminated unions

A result that is either a success or a failure is often written { ok: boolean; value?: T; error?: string }, and then every reader has to wonder whether value is there. A union of two exact shapes, each with a literal tag, says it precisely:

type Lookup = { found: true; owner: Owner } | { found: false; reason: string };

Checking the tag, if (lookup.found), narrows to one shape, and only that shape's fields exist. Forgetting to check is a compile error rather than an undefined at runtime.

Your task

The spec file is fixed; your work is in cats.ts. Press Run early: the compiler reports the spec's type checks first, and each error names a type the task asks you to make precise.

  1. Color is exactly one of 'black', 'white', 'grey' or 'ginger'.
  2. ParseResult is either { ok: true; cat: Cat } or { ok: false; error: string }.
  3. isColor(value: unknown) is a type predicate that is true for exactly the four colors.
  4. parseCat(input: unknown) checks, in order, and fails with the first problem: not an object, where null and arrays are not objects either ("expected an object"); a name that is not a string with something other than spaces ("name must be a non-empty string"); an age that is not a whole number, 0 or more ("age must be a whole number, 0 or more"); a color that is not a Color ("color must be one of black, white, grey, ginger"). Otherwise it succeeds with a new cat holding only those three fields.
  5. summarize(result) answers "Tom (3, grey)" for a cat and "invalid: <error>" for a failure.

When it fails

  • Unused '@ts-expect-error' directive: the spec expects color: 'blue' to be refused, and Color still allows any string.
  • Type 'unknown' is not assignable to type 'Color | null': isColor returns boolean. Make its return type value is Color.
  • Property 'cat' does not exist, or Cat | undefined in an error: ParseResult still has optional fields. Make it a union of two exact shapes.
  • "refuses what is not an object" fails on null or []: typeof says 'object' for both.

Remember

  • Literal unions allow exactly the values that make sense; the compiler refuses the rest.
  • Values from outside are unknown: check before you trust.
  • typeof, ===, Array.isArray and in narrow; a type predicate names a check you reuse.
  • A result with two shapes is a discriminated union, narrowed on its tag.
Stuck? Show a hint

A union of string literals is 'a' | 'b'. A type predicate is a return type of the form value is Color. Narrow unknown in order: typeof input === 'object', then input !== null, then !Array.isArray(input); after that, read its fields as Record<string, unknown> and check each one's typeof before using it. In summarize, once !result.ok has returned, TypeScript knows result.cat exists.