Skip to main content

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}`);
}
}