← TypeScript: types, contracts and diagnosis
02 / 10 · 55 MIN

Unions, narrowing and state models

Trace paths that narrow a union and represent states with required fields, distinguishing static exhaustiveness from external validation.

Possible types change along a path

A union indicates alternatives, not the sum of all available properties. To use an operation, the checker needs to know it exists in every possibility still present at that point. typeof value === "string" can exclude number and justify a toUpperCase call in the corresponding branch.

That knowledge is not a permanent stamp on the variable. A subsequent assignment can change the observed type, provided the new value fits the declared contract. When reading an excerpt, mark the condition taken, early returns and subsequent assignments. Permitted methods depend on the current flow situation.

Early returns and absence

A function returning immediately when x is null leaves only the other union members on continuing paths. The return does not transform null into a string; it prevents that case from reaching the next call. This distinction helps interpret functions with several rejection reasons.

JavaScript retains details respected by the type model. typeof null is "object", so that test alone cannot distinguish an array from null. Truthiness excludes null but also excludes an empty string or zero. A guard should reflect values the contract accepts rather than merely seeking a short way to remove a warning.

Link variants to the data they require

A single object with status: "ok" | "error" and optional value and message fields admits incomplete combinations. Testing status does not automatically make value required. The checker does not infer business relationships undeclared by the type.

A union of separate objects can state that ok requires value and error requires message. The shared discriminant with distinct literals selects the relevant shape. This improves consumer readability and exposes incomplete internal construction. It remains a static representation: a network response needs validation before receiving this contract.

Exhaustiveness reveals missing decisions

After every member of a discriminated union is handled, the remaining path can have type never. Assigning the variable to an explicitly never constant requires that no modeled possibility remains unhandled. If an extension adds pending without changing the switch, the new member becomes visible as an assignment error.

Replacing the assignment with as never changes the line’s role: it asserts impossibility instead of requiring it. A default returning success can also hide new states. The aim is an explicit decision for each variant without confusing this check with validation of incoming external data.

Predicates are contracts to review

A function returning x is string tells the checker how to narrow x when its result is true. The signature can help centralize repeated checks. However, the declaration is not a logical proof that its body implements every necessary condition.

A predicate always returning true can convince a consumer to call a string method on a number. Review must compare the promised type with each acceptance and rejection path. It should also ask whether the function proves a superficial property or the whole advertised contract. External data does not become trustworthy merely because the signature uses predicate syntax.

Guided reading of a delivery decision

The example describes three states already validated at the boundary: accepted includes receipt, rejected includes reason, and pending includes requestId. Each branch reads only the field guaranteed by its variant. No ! is needed because the model represents the relationship between state and data. The function only produces text; it neither performs delivery nor changes a remote decision.

Now imagine a cancelled variant carrying a reason. Identify the line exposing the incomplete extension and the branch needing addition. If an untyped caller sends an unknown state, the default throw can act as a defense, but it does not establish that fields of known variants were validated correctly.

Choose a correction that preserves information

When a diagnostic appears, compare alternatives by the contract they leave to the consumer. Adding a guard may preserve a valid value; redesigning a union may make a relationship explicit; handling a new state may avoid an incorrect conclusion. any, ! and assertions can remove the warning without supplying any of those properties.

In this lesson’s cases, briefing data consists of facts and constraints, and the answer requires selecting a justified revision. Distinguish a modeling failure from external-validation failure. A more precise model improves static analysis but does not by itself establish correctness of every producer or the operational outcome of an action.

// TypeScript 7.0; strict: true; received state already validated.
type Delivery =
  | { status: "accepted"; receipt: string }
  | { status: "rejected"; reason: string }
  | { status: "pending"; requestId: string };

function describe(result: Delivery): string {
  switch (result.status) {
    case "accepted": return "Receipt: " + result.receipt;
    case "rejected": return "Rejected: " + result.reason;
    case "pending": return "Pending: " + result.requestId;
    default: {
      const remaining: never = result;
      throw new Error("Unexpected delivery state");
    }
  }
}
IN PRACTICE

Trace each switch branch. The model links state to required fields. The remaining variable is a static check; throw is a separate runtime defense and does not replace input validation. Executing the excerpt is unnecessary.

Common pitfalls

Confusing optional fields with variants; forgetting null after typeof object; trusting a predicate without checking its body; replacing the never assignment with an assertion.

Related topics: Static types and runtime values · Functions and callable contracts · External data and error boundaries

Take this idea with you

Narrowing follows evidence and paths. A useful model makes relationships needed by consumers explicit.

Create account

Reference: TypeScript Handbook: Narrowing · TypeScript 7.0; explicit strict and host configuration