advanced24 min

unknown, Type Guards, and Safe Assertions

Handle data whose shape you cannot trust, and learn why `as` is the escape hatch of last resort.

What you'll learn

  • Explain the difference between `any` and `unknown`
  • Write a type predicate that narrows an unknown value
  • Describe the risk a type assertion introduces

Prerequisites

Explanation

Data from outside your program — a parsed JSON body, a message from another window — has no type the compiler can trust. TypeScript gives you two ways to describe it, and they are not equivalent.

any versus unknown

any disables checking. Every operation is allowed, including ones that will crash:

const data: any = JSON.parse(text);
data.user.name.toUpperCase(); // compiles; may crash three ways

unknown is the honest version: it accepts any value but permits nothing until you prove what it is.

const data: unknown = JSON.parse(text);
data.user; // rejected — you have not established that data has a user

That rejection is the feature. unknown forces the check that any let you skip.

Type predicates

Ordinary narrowing (typeof, Array.isArray) works on unknown too. For object shapes you write a type guard — a function whose return type is value is T:

interface User { name: string }

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as { name?: unknown }).name === "string"
  );
}

value is User is a type predicate. It tells the compiler: if this returns true, treat the argument as a User from here on.

if (isUser(data)) {
  console.log(data.name.toUpperCase()); // data is User
}

The predicate is a promise you are making. TypeScript cannot verify that the body actually checks what the signature claims — a guard that returns true unconditionally would compile and would be a lie. Keep guards small and obviously correct.

Note the null check. typeof null === "object" in JavaScript, so omitting it is a classic bug.

Type assertions

value as User tells the compiler to stop objecting. It performs no runtime check:

const user = JSON.parse(text) as User;
console.log(user.name.toUpperCase()); // crashes if name is missing

The type error disappears; the bug does not. An assertion is appropriate when you genuinely know something the compiler cannot — and it should be rare, narrow, and commented.

The order of preference: narrow if you can, guard if you must, assert only when you truly know better.

Example

The same untrusted value handled safely with a guard, versus asserted away with no check at all.

interface User {
  name: string;
  age: number;
}

function isUser(value: unknown): value is User {
  if (typeof value !== "object" || value === null) return false;
  const candidate = value as { name?: unknown; age?: unknown };
  return typeof candidate.name === "string" && typeof candidate.age === "number";
}

function describe(value: unknown): string {
  if (isUser(value)) {
    return value.name + " is " + value.age;
  }
  return "Not a user";
}

console.log(describe({ name: "Ada", age: 36 }));
console.log(describe({ name: "Ada" }));
console.log(describe("nonsense"));
console.log(describe(null));

Try it yourself

Remove the `value === null` check, then call describe(null) — a reminder that typeof null is "object".

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Write `isNonEmptyString(value: unknown): value is string` returning true only for strings with at least one character. Remember it must be a type predicate, not just a boolean.

Checks: isNonEmptyString is defined · Accepts non-empty, rejects empty · plus 1 hidden check

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Define `interface Point { x: number; y: number }` and write `isPoint(value: unknown): value is Point` that safely verifies both fields are numbers. It must return false for null.

Checks: isPoint is defined · Accepts a valid point · Rejects partial objects, null, and non-objects

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Reaching for `any` when `unknown` is meant. `any` removes checking; `unknown` defers it until you have proved the shape.
  • Forgetting that `typeof null === "object"`, so an object check without a null check passes for null.
  • Using `as` to silence an error. An assertion changes what the compiler believes, never what the value actually is.

Knowledge check

Knowledge check

1. What can you do with a value typed `unknown` before narrowing it?
2. What does the return type `value is User` provide?
3. What does `JSON.parse(text) as User` actually check at runtime?

Takeaway

`unknown` makes you prove a shape before using it; `as` merely silences the compiler and proves nothing.

Summary

`unknown` accepts anything and permits nothing until narrowed, unlike `any`, which disables checking. Type predicates (`value is T`) let a function narrow for callers, and assertions (`as`) should be rare because they perform no runtime check.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.