Type Guards & Assertions

TypeScript · Lesson 13 of 28 6 min read Updated

In Unions & Narrowing you narrowed types with built-in checks such as typeof and in. Real programs need checks that TypeScript cannot understand on its own: "is this object a valid order?" or "is this list item not empty?". You can teach TypeScript such checks with type guards and assertion functions.

Sometimes you also know more than the compiler, for example which element is on a web page. For that, TypeScript has type assertions (as) and the non-null operator (!). They are useful, but they can also hide bugs. This lesson shows when each tool is the right one.

User-defined type guards

A type guard is a function that returns a boolean and has a special return type, value is SomeType. This is called a type predicate. When the function returns true, TypeScript narrows the argument to that type.

TypeScriptRun it yourself
type Train = { kind: "train"; number: number; name: string };
type Bus = { kind: "bus"; operator: string; seats: number };
type Trip = Train | Bus;

function isTrain(trip: Trip): trip is Train {
  return trip.kind === "train";
}

const trips: Trip[] = [
  { kind: "train", number: 12301, name: "Rajdhani" },
  { kind: "bus", operator: "RSRTC", seats: 40 },
  { kind: "train", number: 12015, name: "Shatabdi" },
];

for (const trip of trips) {
  if (isTrain(trip)) {
    console.log(`Train ${trip.number} ${trip.name}`);
  } else {
    console.log(`Bus by ${trip.operator}, ${trip.seats} seats`);
  }
}

const trains = trips.filter(isTrain); // type: Train[]
console.log(trains.map((t) => t.name).join(" + "));
Output
Train 12301 Rajdhani
Bus by RSRTC, 40 seats
Train 12015 Shatabdi
Rajdhani + Shatabdi

The guard works in if statements and also in array methods: trips.filter(isTrain) returns Train[], not Trip[], so you can use t.name without another check.

A type guard is a promise you make to the compiler. TypeScript does not check that your function body really proves the type. If isTrain returned true for buses, TypeScript would believe it. Keep guards short and obviously correct, and test them.

Assertion functions

A type guard returns true or false and lets you decide what to do. An assertion function instead throws when the check fails, and after it returns, TypeScript narrows the type for the rest of the code. Its return type uses the asserts keyword:

TypeScriptRun it yourself
type Booking = { pnr: string; passenger: string; seat?: number };

function assertSeated(b: Booking): asserts b is Booking & { seat: number } {
  if (b.seat === undefined) {
    throw new Error(`PNR ${b.pnr}: no seat allotted yet`);
  }
}

function assert(condition: unknown, message: string): asserts condition {
  if (!condition) throw new Error(message);
}

function boardingPass(b: Booking): string {
  assert(b.passenger.length > 0, "Passenger name is empty");
  assertSeated(b);
  return `${b.passenger}, seat ${b.seat.toString().padStart(2, "0")}`;
}

console.log(boardingPass({ pnr: "4521", passenger: "Meera", seat: 7 }));
try {
  console.log(boardingPass({ pnr: "4522", passenger: "Kabir" }));
} catch (err) {
  console.log(err instanceof Error ? err.message : err);
}
Output
Meera, seat 07
PNR 4522: no seat allotted yet

After assertSeated(b), TypeScript knows b.seat is a number, so b.seat.toString() needs no extra check. asserts condition is the general form: after assert(x !== null, ...) returns, x is known not to be null. Assertion functions are handy at the start of a function, to check inputs and keep the rest of the code clean.

Type assertions with as

A type assertion tells the compiler "treat this value as this type; trust me". It changes nothing at run time; it only changes what TypeScript believes. The classic example is the DOM: TypeScript knows querySelector returns some Element (or null), but you know it is an <input>.

TypeScript
const qtyInput = document.querySelector("#qty") as HTMLInputElement;
const qty = Number(qtyInput.value); // .value exists on HTMLInputElement

const pnr = "PNR123" as number;
Output
app.ts(4,13): error TS2352: Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

TypeScript allows an assertion only between types that overlap, such as Element and HTMLInputElement. A string can never be a number, so "PNR123" as number is refused. You can force it with as unknown as number, but that is almost always a bug waiting to happen.

The danger of as is that it silences the compiler. If someone renames #qty in the HTML, qtyInput is null at run time, yet TypeScript believes it is an input. A check is safer:

TypeScript
const el = document.querySelector("#qty");
if (el instanceof HTMLInputElement) {
  console.log(Number(el.value)); // narrowed by a real check
}

The non-null assertion operator

A ! after an expression removes null and undefined from its type. It is a short type assertion that says "this is definitely not empty".

TypeScriptRun it yourself
const stock = new Map<string, number>([
  ["notebook", 120],
  ["pen", 300],
]);

const pens = stock.get("pen")!; // type: number (we just added it)
console.log(pens + 50);

const pencils = stock.get("pencil"); // type: number | undefined
console.log(pencils ?? 0);

const markers = stock.get("marker")!; // wrong! the key does not exist
console.log(markers + 10);
Output
350
0
NaN

The last two lines show the risk: ! told TypeScript that markers was a number, but it was undefined, so the result is NaN and nobody was warned. Use ?? with a default, or a real check, whenever the value might truly be missing. Keep ! for cases where you are certain, such as a key you added on the line above.

Common mistakes

  • Writing a guard that lies. value is Student is not checked. If the body checks too little, TypeScript trusts a wrong answer.
  • Using as on data from an API. await res.json() as Student checks nothing. Validate with a type guard instead.
  • Chaining as unknown as X to silence an error. The error was trying to tell you something.
  • Sprinkling ! everywhere. Each ! is a possible crash or NaN. Prefer ?., ?? or an if check.

Key points

  • A type guard has the return type value is T and narrows the argument when it returns true; it also narrows arrays in filter.
  • TypeScript 5.5+ infers simple predicates such as (x) => x != null automatically.
  • An assertion function (asserts value is T or asserts condition) throws on failure and narrows the code after the call.
  • value as T changes only what TypeScript believes; it is allowed only between overlapping types and checks nothing at run time.
  • The ! operator removes null and undefined from a type; use it only when you are certain.

Exercise

An attendance app in Kochi receives a list of records where some entries are broken. Write a type Attendance = { rollNo: number; present: boolean } and a type guard isAttendance(x: unknown): x is Attendance. Use it with filter on the array [{ rollNo: 1, present: true }, null, { rollNo: 2 }, { rollNo: 3, present: false }, "absent"] and log how many students were present out of the valid records.

Show solution
TypeScript
type Attendance = { rollNo: number; present: boolean };

function isAttendance(x: unknown): x is Attendance {
  return (
    typeof x === "object" && x !== null &&
    "rollNo" in x && typeof x.rollNo === "number" &&
    "present" in x && typeof x.present === "boolean"
  );
}

const raw: unknown[] = [{ rollNo: 1, present: true }, null, { rollNo: 2 }, { rollNo: 3, present: false }, "absent"];
const records = raw.filter(isAttendance); // Attendance[]
const present = records.filter((r) => r.present).length;

console.log(`${present} of ${records.length} present`); // 1 of 2 present

The guard checks every property it promises, so filter(isAttendance) returns a properly typed Attendance[] that is safe to use.

Quick Quiz

1. What does the return type trip is Train mean?

2. What does an assertion function with asserts value is T do when the check fails?

3. Why is "PNR123" as number an error?

4. What does stock.get("marker")! do if the key does not exist?

Learn TypeScript with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call