Type-safe application code

TypeScript Narrowing and Type Guards

Refine union and unknown values through runtime evidence, discriminated unions, predicates, and exhaustive handling.

How this page is maintained

Written for learners, checked against the sources below, and reviewed every quarter. Last reviewed July 27, 2026.

Short answer

TypeScript narrowing uses runtime checks and control flow to refine a broad static type within a branch. Built-in guards include typeof, equality, property checks, and instanceof. Discriminated unions make application states explicit. User-defined predicates can package repeated checks, but their implementation must actually prove the claimed type. External data still requires runtime validation.

Who this is for: JavaScript and TypeScript developers who encounter union types, unknown API data, optional values, or unsafe type assertions.

  • Start untrusted values as unknown and narrow them with checks that exist at runtime.
  • Use discriminated unions for states whose allowed fields and transitions differ.
  • Prefer exhaustive handling and validation over assertions that merely silence the compiler.

Control flow carries type evidence

When code checks typeof value === string, TypeScript knows that value is a string inside the matching branch. Equality checks can correlate values, truthiness can remove nullish cases, and the in operator can show that a property exists. Narrowing follows returns and assignments, so an early rejection can simplify the remaining function.

Each guard has runtime limits. typeof null is object, arrays are objects, and instanceof depends on runtime constructors and realms. A property existing does not prove every nested field has the expected shape. Choose a check that establishes exactly what the next operation needs, and preserve explicit handling for null, arrays, and malformed objects.

Discriminated unions model real states

A discriminated union gives each variant a shared literal field such as status. A loading value has no data, a success value has data, and a failure value has an error. Switching on status narrows the other fields automatically and prevents impossible combinations such as success with both data and an active error.

Use a never assignment in a default branch when every variant must be handled. Adding a new state then creates a compile-time error at incomplete switches. This is valuable for payment states, asynchronous screens, domain commands, and event payloads. It is less useful when variants do not have a stable conceptual distinction.

Custom guards are executable claims

A function returning value is Widget tells TypeScript to trust its boolean result as proof. The compiler does not inspect whether the implementation fully validates Widget. A guard that checks only for an id but claims a complex nested object can introduce the same runtime failure as a direct assertion, with more misleading confidence.

Keep guards small and test them with valid values, missing fields, wrong primitive types, null, arrays, and hostile nested shapes. For substantial external schemas, use a maintained validation approach and derive static types where the project pattern supports it. Validation should report useful boundaries without copying confidential payloads into errors.

Assertions have a narrow role

The as syntax changes the compiler's view but performs no runtime conversion or validation. It is appropriate when code has evidence TypeScript cannot represent, such as a platform guarantee at a carefully reviewed boundary. It should not be the default response to an inconvenient error from API data or a broad DOM query.

Prefer changing the source type, adding a guard, or redesigning the state model. Avoid double assertions through unknown because they can claim unrelated types with no proof. Enable strict compiler settings appropriate to the codebase and fix errors by making assumptions visible. Compiler strictness reduces classes of defects but does not validate network or stored data.

Parse a job result safely

An API returns unknown JSON representing a queued, completed, or failed export job.

  1. Keep the parsed value as unknown and first require a non-null, non-array object with a string status field.
  2. For queued, validate a job identifier; for completed, validate the identifier and a permitted HTTPS download URL; for failed, validate a safe error code.
  3. Return a discriminated JobResult union only after the matching variant's fields pass runtime checks.
  4. Render the union with an exhaustive switch so every state has a deliberate interface.
  5. Test malformed objects, unknown statuses, omitted fields, arrays, and valid examples for each variant.
Result: Untrusted JSON cannot enter the application as a trusted job result, and a future status forces consumers to decide how it should behave.

Narrowing choice guide

Select the smallest runtime proof that supports the next operation.

  • Primitive: use typeof plus explicit null handling where object is possible.
  • Variant: use a literal discriminant and an exhaustive switch over known states.
  • Property: verify object shape, ownership expectations, and the property's own runtime type.
  • Class instance: use instanceof only when constructor identity is a valid cross-boundary assumption.
  • External payload: validate the complete required schema, return typed success or structured failure, and test malformed values.

Common mistakes

  • Replacing unknown with any so property access compiles without evidence.
  • Writing a type predicate that checks one field but claims an entire complex object.
  • Using a type assertion as though it parses, converts, or validates runtime data.

Try one

A function receives string | string[] | undefined and must return one trimmed nonempty string or null. Explain the branches and why an assertion is unnecessary.

The function first handles undefined, then uses Array.isArray to select an array policy such as accepting exactly one item, leaving the remaining branch as string. It trims the selected string and returns null when empty. Each runtime check supplies TypeScript with evidence, so as string would hide unresolved array and absence behavior rather than solve it.

Sources

Learn this with a tutor

Tell LearnLive what you already know and what you need to do with typescript narrowing.

Build this course