unknown & Type Guards

The Course
•
5 min read

A value from outside your checked program may not match your expectations. TypeScript’s unknown type makes that uncertainty explicit. You can store any value in unknown, but cannot read arbitrary properties or call it until you narrow it.

Compare unknown with any. Any allows unchecked operations to continue; unknown asks for evidence. At a boundary, unknown is usually the honest starting point. Inside trusted application logic, use a specific domain type after validation.

Start with ordinary JavaScript guards: typeof for primitive values, a null check for objects, and Array.isArray when you need to distinguish lists. Remember that typeof null is “object”. Check null explicitly before treating a value as a record.

A type predicate such as value is Session describes the result of a guard function. When the function returns true, the checker trusts that claim. It does not prove your guard inspected every necessary field. A mistaken predicate can create false confidence, so keep guards small, check every promised property, and test malformed examples.

A boolean return alone can be useful, but an explicit predicate communicates narrowing to callers. Today you will first recognize a primitive, then validate whole records and domain rules. Do not use as Session to skip this work: an assertion changes what the checker believes, not the 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
function isText(value: unknown): value is string {
  return typeof value === "string";
}

function display(value: unknown): string {
  if (isText(value)) {
    // The guard established that value is a string in this branch.
    return value.trim();
  }

  return "Not text";
}

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

Enable JavaScript to edit and type-check this example.

Check your prediction
Types
Not text

Read a real compiler error

The initializer looks familiar, but the declared boundary type intentionally withholds that promise.

Find and fix the type errorTypeScript
const value: unknown = { topic: "Types" };

console.log(value.topic);

Enable JavaScript to edit and type-check this example.

Check your diagnosis

Narrow to a non-null object and verify the topic property before reading it as text. Or, for a genuinely trusted local literal, let the checker infer its actual shape. Do not assert untrusted values into the shape you wish they had.

A guard is trusted evidence and needs honest checks

Unknown prevents using a value until evidence narrows it. A type predicate tells the checker that a successful call establishes a particular shape. The checker trusts that promise; it does not prove that the guard’s body inspected every required property.

Keep predicates small and test both accepted and rejected values. A guard that merely checks typeof object still accepts null or unrelated records, depending on its exact condition. Use ordinary runtime checks to justify the stronger type you claim.

A guard is trusted evidence and needs honest checks: traceTypeScript
function isText(value: unknown): value is string {
  return typeof value === "string";
}

const inputs: unknown[] = ["Types", 25, null];

for (const value of inputs) {
  console.log(isText(value) ? value.toUpperCase() : "Not text");
}

Enable JavaScript to edit and type-check this example.

Check your prediction
TYPES
Not text
Not text

The predicate narrows each individual value in the true branch. A type assertion would let the call compile without establishing the same runtime fact.

A guard is trusted evidence and needs honest checks: contrastTypeScript
function isNonblank(value: unknown): value is string {
  return typeof value === "string" && value.trim() !== "";
}

console.log(isNonblank("   "));
console.log(isNonblank(" Types "));
console.log(isNonblank(25));

Enable JavaScript to edit and type-check this example.

Check your prediction
false
true
false

This guard checks a stronger runtime rule than string alone expresses. It accepts padded text without transforming it; a decoder can return a normalized new value when that is needed.

Try the rule on new inputs

Test blank strings, null, arrays and records. Deliberately weaken a guard and identify which negative fixture exposes it. Explain why returning true from a predicate can be more dangerous than a normal boolean if its claim is wrong.

Your turn

Exercise

unknown & Type Guards: practise

Write isNonNegativeNumber(value: unknown): value is number. Require typeof number, Number.isFinite and value >= 0. Use it to print valid for 0 and invalid for “25”, -1 and NaN.

Your workspaceTypeScript
// Guard unknown before numeric operations; test four cases.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

The primitive check enables safe numeric checks; neither alone proves the domain rule.

Hint 2: a first step

Use a conjunction whose first part checks typeof value === “number”.

Show one solution
One possible solutionTypeScript
function isNonNegativeNumber(value: unknown): value is number {
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
}

for (const value of [0, "25", -1, NaN]) {
  console.log(isNonNegativeNumber(value) ? "valid" : "invalid");
}

Enable JavaScript to edit and type-check this example.

valid
invalid
invalid
invalid

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

Test Infinity, null, false and 2.5. Explain why a guard that only checks typeof number would accept NaN. Then deliberately make the predicate always return true: the checker trusts it, but your tests should show why that implementation is dishonest.

Was this article helpful?

Your response is saved on this device.