Files
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:
| Check | What TypeScript learns |
|---|---|
typeof x === 'string' | x is a string (also 'number', 'boolean', 'object', 'function') |
x === null, x !== undefined | the value is, or is not, that one |
Array.isArray(x) | x is an array |
'email' in x | x 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.
Coloris exactly one of'black','white','grey'or'ginger'.ParseResultis either{ ok: true; cat: Cat }or{ ok: false; error: string }.isColor(value: unknown)is a type predicate that is true for exactly the four colors.parseCat(input: unknown)checks, in order, and fails with the first problem: not an object, wherenulland arrays are not objects either ("expected an object"); anamethat is not a string with something other than spaces ("name must be a non-empty string"); anagethat is not a whole number, 0 or more ("age must be a whole number, 0 or more"); acolorthat 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.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 expectscolor: 'blue'to be refused, andColorstill allows any string.Type 'unknown' is not assignable to type 'Color | null':isColorreturnsboolean. Make its return typevalue is Color.Property 'cat' does not exist, orCat | undefinedin an error:ParseResultstill has optional fields. Make it a union of two exact shapes.- "refuses what is not an object" fails on
nullor[]:typeofsays'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.isArrayandinnarrow; 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.
Press Run tests to start the app. Its log appears here.Tests
- cats.spec.tsrun to see its tests