Type Guards & Narrowing
Introduction
Type guards are TypeScript's way of narrowing types within conditional blocks. They help TypeScript understand what type a value has based on runtime checks, allowing you to write safer code with better type inference.
Type narrowing is the process of refining a broad type (like string | number) into a more specific type (like string) based on conditional logic.
The Problem
function processValue(value: string | number) {
// TypeScript doesn't know which type value is
console.log(value.toUpperCase()); // ❌ Error: Property 'toUpperCase' does not exist on type 'number'
console.log(value.toFixed(2)); // ❌ Error: Property 'toFixed' does not exist on type 'string'
}
The Solution: Type Guards
function processValue(value: string | number) {
if (typeof value === "string") {
// TypeScript knows value is string here
console.log(value.toUpperCase()); // ✅ Works!
} else {
// TypeScript knows value is number here
console.log(value.toFixed(2)); // ✅ Works!
}
}
Concepts
typeof Type Guards
Use typeof to check primitive types:
function printValue(value: string | number | boolean) {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else if (typeof value === "number") {
console.log(value.toFixed(2));
} else {
console.log(value ? "true" : "false");
}
}
printValue("hello"); // "HELLO"
printValue(42.567); // "42.57"
printValue(true); // "true"
typeof returns:
"string"for strings"number"for numbers"boolean"for booleans"undefined"for undefined"object"for objects (and null!)"function"for functions"symbol"for symbols"bigint"for bigints
instanceof Type Guards
Use instanceof to check class instances:
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // TypeScript knows it's a Dog
} else {
animal.meow(); // TypeScript knows it's a Cat
}
}
makeSound(new Dog()); // "Woof!"
makeSound(new Cat()); // "Meow!"
in Operator Type Guards
Use in to check if a property exists:
interface Car {
drive: () => void;
wheels: number;
}
interface Boat {
sail: () => void;
anchors: number;
}
function operate(vehicle: Car | Boat) {
if ("drive" in vehicle) {
// TypeScript knows vehicle is Car
vehicle.drive();
console.log(`Has ${vehicle.wheels} wheels`);
} else {
// TypeScript knows vehicle is Boat
vehicle.sail();
console.log(`Has ${vehicle.anchors} anchors`);
}
}
Equality Narrowing
TypeScript narrows types when you compare values:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// Both must be string (the only common type)
console.log(x.toUpperCase());
console.log(y.toUpperCase());
}
}
function processValue(value: string | null) {
if (value !== null) {
// TypeScript knows value is string
console.log(value.toUpperCase());
}
}
Truthiness Narrowing
TypeScript understands truthiness checks:
function printLength(str: string | null | undefined) {
if (str) {
// TypeScript knows str is string (not null or undefined)
console.log(str.length);
} else {
console.log("No string provided");
}
}
function getUserName(user: { name: string } | null) {
if (user) {
console.log(user.name); // TypeScript knows user is not null
}
}
Custom Type Guards (Type Predicates)
Create your own type guards with is:
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
// Custom type guard
function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim !== undefined;
}
function move(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim(); // TypeScript knows it's Fish
} else {
animal.fly(); // TypeScript knows it's Bird
}
}
Discriminated Unions
Use a literal property to discriminate:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape): number {
if (shape.kind === "circle") {
// TypeScript knows it's Circle
return Math.PI * shape.radius ** 2;
} else {
// TypeScript knows it's Square
return shape.sideLength ** 2;
}
}
// Also works with switch
function getPerimeter(shape: Shape): number {
switch (shape.kind) {
case "circle":
return 2 * Math.PI * shape.radius;
case "square":
return 4 * shape.sideLength;
}
}
Array.isArray()
TypeScript understands Array.isArray():
function processValue(value: string | string[]) {
if (Array.isArray(value)) {
// TypeScript knows value is string[]
value.forEach(item => console.log(item));
} else {
// TypeScript knows value is string
console.log(value.toUpperCase());
}
}
Examples
Validation with Type Guards
interface User {
id: number;
name: string;
email: string;
}
// Type guard for User
function isUser(obj: any): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
typeof obj.id === "number" &&
typeof obj.name === "string" &&
typeof obj.email === "string"
);
}
function processUserData(data: unknown): void {
if (isUser(data)) {
// TypeScript knows data is User
console.log(`User: ${data.name} (${data.email})`);
} else {
console.log("Invalid user data");
}
}
// Test
processUserData({ id: 1, name: "Alice", email: "alice@example.com" });
processUserData({ name: "Bob" }); // Invalid - missing fields
processUserData("not an object"); // Invalid
API Response Handler
interface SuccessResponse<T> {
status: "success";
data: T;
}
interface ErrorResponse {
status: "error";
message: string;
code: number;
}
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
// Type guard for success response
function isSuccess<T>(response: ApiResponse<T>): response is SuccessResponse<T> {
return response.status === "success";
}
// Type guard for error response
function isError<T>(response: ApiResponse<T>): response is ErrorResponse {
return response.status === "error";
}
async function handleApiResponse<T>(response: ApiResponse<T>): Promise<void> {
if (isSuccess(response)) {
console.log("Success! Data:", response.data);
} else if (isError(response)) {
console.error(`Error ${response.code}: ${response.message}`);
}
}
// Usage
const successResponse: ApiResponse<{ name: string }> = {
status: "success",
data: { name: "Alice" }
};
const errorResponse: ApiResponse<any> = {
status: "error",
message: "Not found",
code: 404
};
handleApiResponse(successResponse);
handleApiResponse(errorResponse);
Safe JSON Parsing
interface ParsedData {
name: string;
age: number;
email: string;
}
function isParsedData(obj: any): obj is ParsedData {
return (
typeof obj === "object" &&
obj !== null &&
typeof obj.name === "string" &&
typeof obj.age === "number" &&
typeof obj.email === "string"
);
}
function parseJSON(json: string): ParsedData | null {
try {
const parsed = JSON.parse(json);
if (isParsedData(parsed)) {
return parsed;
} else {
console.error("Invalid data structure");
return null;
}
} catch (error) {
console.error("Invalid JSON");
return null;
}
}
// Usage
const validJSON = '{"name":"Alice","age":30,"email":"alice@example.com"}';
const invalidJSON = '{"name":"Bob","age":"thirty"}';
const data1 = parseJSON(validJSON);
if (data1) {
console.log(data1.name); // TypeScript knows data1 is ParsedData
}
const data2 = parseJSON(invalidJSON);
// Will be null due to invalid structure
Challenges
1. Number or String Processor
- Function that accepts number or string
- Use typeof to handle each differently
- Return appropriate formatted result
💡 Show Answer
function formatValue(value: number | string): string {
if (typeof value === "number") {
return `Number: ${value.toFixed(2)}`;
} else {
return `String: ${value.toUpperCase()}`;
}
}
console.log(formatValue(42.567)); // "Number: 42.57"
console.log(formatValue("hello")); // "String: HELLO"
2. Animal Type Guard
- Dog and Cat interfaces
- Create isDog type predicate
- Function that uses type guard
💡 Show Answer
interface Dog {
name: string;
breed: string;
bark: () => void;
}
interface Cat {
name: string;
color: string;
meow: () => void;
}
function isDog(animal: Dog | Cat): animal is Dog {
return (animal as Dog).bark !== undefined;
}
function makeSound(animal: Dog | Cat): void {
if (isDog(animal)) {
animal.bark();
console.log(`Breed: ${animal.breed}`);
} else {
animal.meow();
console.log(`Color: ${animal.color}`);
}
}