Type Narrowing

The Course
•
5 min read

Narrowing is how the checker uses conditions to refine a type. If a parameter starts as string | number, then inside typeof value === "string" it can be treated as string. In the other branch it is number. The condition runs at runtime too; it is evidence, not a promise pasted onto a value.

typeof is the JavaScript operator that returns a text label such as “string” or “number”. toFixed(1), used in the exercise, formats a number with one digit after the decimal point and returns a string: 2 becomes "2.0". It does not change the original number.

Use operations supported by the current narrowed type. Outside the branch, only operations valid for the whole union are safe. Do not call a string method just because one of the possible types has it.

Objects can carry a discriminant: a common field with a different literal value for each variant. A result could be { kind: "ok"; minutes: number } or { kind: "error"; message: string }. Checking kind tells both JavaScript and TypeScript which other fields are present. This is called a discriminated union.

Keep variant-specific data inside its variant. A single record with optional minutes and optional message permits confusing combinations: both missing or both supplied. Two explicit shapes represent the intended alternatives more clearly.

The result below is an ordinary object returned immediately by a function. It describes success or failure as data for the caller to inspect, instead of throwing an exception. You already know if/else, so you can use it immediately. Later validation lessons will return this kind of result for messy data.

Predict, check, then run

Before running, write down the output and explain which information the checker knows. Click Check types first, then Check & run.

Predict, then runTypeScript
type Result =
  | {
      kind: "ok";

      minutes: number;
    }
  | {
      kind: "error";

      message: string;
    };

function describe(result: Result): string {
  if (result.kind === "ok") {
    return `${result.minutes} min`;
  }

  return result.message;
}

console.log(describe({ kind: "ok", minutes: 20 }));
console.log(describe({ kind: "error", message: "Try again" }));

Enable JavaScript to edit and type-check this example.

Check your prediction
20 min
Try again

Read a real compiler error

Even though this particular call uses text, the function promises to accept numbers too.

Find and fix the type errorTypeScript
function display(value: string | number): string {
  return value.trim();
}

console.log(display(" Types "));

Enable JavaScript to edit and type-check this example.

Check your diagnosis

The body must be safe for every accepted input. Narrow with typeof before trim, and supply behavior for numbers. Changing only the demonstration call does not fix the contract.

Let the discriminant select the available data

A tagged union keeps each outcome paired with its fields. Checking the tag is evidence about the whole record, not just about one string property. Keep that relationship intact until you have selected the branch that owns the data you want.

A boolean truthiness test is often weaker than a discriminant check. A successful result can contain zero or empty text. Model success explicitly so those meaningful values do not accidentally become errors in the formatter.

Let the discriminant select the available data: traceTypeScript
type Result =
  | {
      kind: "ok";

      value: number;
    }
  | {
      kind: "error";

      message: string;
    };

function label(result: Result): string {
  if (result.kind === "ok") {
    return String(result.value);
  }

  return result.message;
}

console.log(label({ kind: "ok", value: 0 }));
console.log(label({ kind: "error", message: "Try again" }));

Enable JavaScript to edit and type-check this example.

Check your prediction
0
Try again

Zero remains a success because kind decides the branch. Checking result.value truthiness would not express the outcome and is not valid on the error member anyway.

Let the discriminant select the available data: contrastTypeScript
function count(value: string | number): number {
  if (typeof value === "string") {
    return value.length;
  }

  return value;
}

console.log(count("Types"));
console.log(count(0));

Enable JavaScript to edit and type-check this example.

Check your prediction
5
0

After the string branch returns, only number remains. Narrowing can follow early returns; it does not require deeply nested if blocks.

Try the rule on new inputs

Add another result example with an empty successful string in a separate string-valued union. Explain how its tag preserves success. Then remove a narrowing check and read the property-access error rather than casting it away.

Your turn

Exercise

Type Narrowing: practise

Write display(value: string | number): string. Trim a string; format a number with toFixed(1). Print Types for ” Types ” and 2.0 for 2.

Your workspaceTypeScript
// Use typeof to choose the safe operation before returning.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

The runtime condition gives the checker evidence about which member is present.

Hint 2: a first step

Begin with a branch testing typeof value === “string”.

Show one solution
One possible solutionTypeScript
function display(value: string | number): string {
  if (typeof value === "string") {
    return value.trim();
  }

  return value.toFixed(1);
}

console.log(display("  Types  "));
console.log(display(2));

Enable JavaScript to edit and type-check this example.

Types
2.0

Explain the types at the function boundary and the runtime checks, if any. Compare your approach before copying this answer.

Change it and explain it

Add a boolean alternative and give it an explicit branch. Explain why a cast would not convert a boolean to a number. Then draw the two Result shapes from the example and identify their discriminant.

Was this article helpful?

Your response is saved on this device.