Type Guards
Type Guards
Section titled “Type Guards”What it is
Section titled “What it is”Type guards are TypeScript constructs that narrow down the type of a variable within a conditional block, enabling type-safe code.
Before this feature
Section titled “Before this feature”Had to use type assertions:
function process(value: string | number) { // TypeScript doesn't know which type const len = (value as string).length; // Unsafe!}After this feature
Section titled “After this feature”Type guards provide safe type narrowing:
// typeof type guardfunction format(value: string | number): string { if (typeof value === "string") { // TypeScript knows value is string here return value.toUpperCase(); } else { // TypeScript knows value is number here return value.toFixed(2); }}
// instanceof type guardclass Dog { bark() { console.log("Woof!"); }}class Cat { meow() { console.log("Meow!"); }}
function makeSound(animal: Dog | Cat) { if (animal instanceof Dog) { animal.bark(); } else { animal.meow(); }}
// in operator type guardinterface Car { drive(): void;}interface Boat { sail(): void;}
function move(vehicle: Car | Boat) { if ("drive" in vehicle) { vehicle.drive(); } else { vehicle.sail(); }}
// Custom type guard (type predicate)function isString(value: unknown): value is string { return typeof value === "string";}
function process(value: unknown) { if (isString(value)) { // TypeScript knows value is string console.log(value.toUpperCase()); }}
// Array type guardfunction isStringArray(value: unknown): value is string[] { return Array.isArray(value) && value.every(item => typeof item === "string");}
// Discriminated unionstype Shape = | { kind: "circle"; radius: number } | { kind: "rectangle"; width: number; height: number };
function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "rectangle": return shape.width * shape.height; }}Why this is better
Section titled “Why this is better”- Type safety: Compile-time guarantees
- Autocomplete: IDE knows exact type
- No assertions: Avoid unsafe type assertions
- Readable: Clear type checking logic
- Maintainable: Compiler catches errors
Key notes / edge cases
Section titled “Key notes / edge cases”- Custom guards must return
value is Type - Guards only work within their scope
- Can combine multiple guards
- Discriminated unions use literal types
- Nullable type guards
// Truthiness narrowingfunction print(value: string | null | undefined) { if (value) { // value is string here console.log(value.toUpperCase()); }}
// Equality narrowingfunction compare(x: string | number, y: string | boolean) { if (x === y) { // Both must be string here x.toUpperCase(); y.toUpperCase(); }}
// Never type with exhaustive checkingfunction assertNever(value: never): never { throw new Error(`Unexpected value: ${value}`);}
type Action = | { type: "increment" } | { type: "decrement" };
function reducer(action: Action) { switch (action.type) { case "increment": return; case "decrement": return; default: assertNever(action); // Ensures all cases covered }}Quick practice
Section titled “Quick practice”-
Create a type guard for checking if value is a number array
Answer
function isNumberArray(value: unknown): value is number[] {return Array.isArray(value) &&value.every(item => typeof item === "number");} -
Narrow
string | nullto juststringAnswer
function process(value: string | null) {if (value !== null) {// value is string hereconsole.log(value.toUpperCase());}} -
What’s a discriminated union?
Answer
A union type where each variant has a common property (discriminant) with a unique literal type, enabling type narrowing via that property.