Type Guards & Assertions
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.
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(" + "));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:
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);
}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>.
const qtyInput = document.querySelector("#qty") as HTMLInputElement;
const qty = Number(qtyInput.value); // .value exists on HTMLInputElement
const pnr = "PNR123" as number;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:
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".
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);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 Studentis not checked. If the body checks too little, TypeScript trusts a wrong answer. - Using
ason data from an API.await res.json() as Studentchecks nothing. Validate with a type guard instead. - Chaining
as unknown as Xto silence an error. The error was trying to tell you something. - Sprinkling
!everywhere. Each!is a possible crash orNaN. Prefer?.,??or anifcheck.
Key points
- A type guard has the return type
value is Tand narrows the argument when it returnstrue; it also narrows arrays infilter. - TypeScript 5.5+ infers simple predicates such as
(x) => x != nullautomatically. - An assertion function (
asserts value is Torasserts condition) throws on failure and narrows the code after the call. value as Tchanges only what TypeScript believes; it is allowed only between overlapping types and checks nothing at run time.- The
!operator removesnullandundefinedfrom 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
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 presentThe 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?
A type predicate turns a boolean function into a type guard.
2. What does an assertion function with asserts value is T do when the check fails?
Assertion functions throw on failure and narrow the type after they return.
3. Why is "PNR123" as number an error?
Assertions are only allowed between types that could be related.
4. What does stock.get("marker")! do if the key does not exist?
! only changes the type; it never checks the value.
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
