TypeScript

Type Assertions & Type Guards

27 min Lesson 10 of 40

Type Assertions & Type Guards

TypeScript provides powerful mechanisms to work with types at runtime. Type assertions allow you to tell the compiler "trust me, I know what I'm doing," while type guards let you narrow types safely based on runtime checks. Understanding these concepts is essential for writing flexible yet type-safe TypeScript code.

Type Assertions

Type assertions are a way to tell TypeScript that you know more about a value's type than it does. They don't perform any runtime checks or conversions; they're purely for the compiler.

Type Assertions with 'as' Keyword:

// Basic type assertion
let someValue: unknown = 'Hello, TypeScript!';

// Assert that someValue is a string
let strLength: number = (someValue as string).length;
console.log(strLength); // 18

// Type assertion with DOM elements
const inputElement = document.getElementById('username') as HTMLInputElement;
inputElement.value = 'john_doe';

// Type assertion with fetch response
async function fetchUser(id: string): Promise<void> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  // Assert the shape of the data
  const user = data as {
    id: string;
    name: string;
    email: string;
  };

  console.log(user.name);
}

// Type assertion with object literals
interface Point {
  x: number;
  y: number;
}

const point = { x: 10, y: 20, z: 30 } as Point;
// Note: 'z' property is ignored by the type system

// Multiple assertions (chain)
let value: unknown = '123';
let numValue = (value as unknown) as number; // Not recommended, but possible
Warning: Type assertions bypass TypeScript's type checking. Use them sparingly and only when you're certain about the type. Incorrect assertions can lead to runtime errors.

Angle-Bracket Syntax (Alternative)

TypeScript also supports angle-bracket syntax for type assertions, though as is preferred in JSX/TSX files:

Angle-Bracket Syntax:

// Angle-bracket syntax (equivalent to 'as')
let someValue: unknown = 'Hello!';
let strLength: number = (<string>someValue).length;

// Both syntaxes are equivalent
let value1 = someValue as string;
let value2 = <string>someValue;

// However, 'as' syntax is required in .tsx files
// because <> conflicts with JSX syntax
Tip: Prefer the as syntax for type assertions. It's more consistent across TypeScript and TSX files, and it's the modern recommended approach.

Non-null Assertion Operator

The non-null assertion operator (!) tells TypeScript that a value is not null or undefined:

Non-null Assertion:

// Function that might return null
function findUser(id: string): { name: string; email: string } | null {
  // Simulate database lookup
  if (id === '123') {
    return { name: 'John', email: 'john@example.com' };
  }
  return null;
}

// Using non-null assertion
const user = findUser('123')!; // Assert that result is not null
console.log(user.name); // No error

// Without non-null assertion
const user2 = findUser('123');
// console.log(user2.name); // Error: Object is possibly 'null'

// Non-null assertion with optional chaining
interface Config {
  apiKey?: string;
}

const config: Config = { apiKey: 'secret-123' };

// Assert that apiKey exists
const key: string = config.apiKey!;
console.log(key.toUpperCase());

// DOM element example
const button = document.getElementById('submit-btn')!;
button.addEventListener('click', () => {
  console.log('Button clicked');
});
Warning: The non-null assertion operator is dangerous. If the value is actually null or undefined, you'll get a runtime error. Use it only when you're absolutely certain the value exists.

Type Guards - typeof

The typeof operator is a built-in JavaScript operator that TypeScript uses for type narrowing:

typeof Type Guard:

// typeof with primitive types
function processValue(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);
  }
}

console.log(processValue('hello')); // "HELLO"
console.log(processValue(3.14159)); // "3.14"

// typeof with multiple types
function printValue(value: string | number | boolean | undefined): void {
  if (typeof value === 'string') {
    console.log(`String: ${value}`);
  } else if (typeof value === 'number') {
    console.log(`Number: ${value}`);
  } else if (typeof value === 'boolean') {
    console.log(`Boolean: ${value}`);
  } else {
    console.log('Value is undefined');
  }
}

// typeof limitations
const arr = [1, 2, 3];
console.log(typeof arr); // "object" (not helpful for arrays)

const obj = { x: 10 };
console.log(typeof obj); // "object"

const nullValue = null;
console.log(typeof nullValue); // "object" (JavaScript quirk)
Note: typeof works well for primitive types but has limitations with objects, arrays, and null. For these cases, use other type guards.

Type Guards - instanceof

The instanceof operator checks if an object is an instance of a specific class:

instanceof Type Guard:

// Classes for demonstration
class Dog {
  bark(): void {
    console.log('Woof!');
  }
}

class Cat {
  meow(): void {
    console.log('Meow!');
  }
}

// Function using instanceof
function makeSound(animal: Dog | Cat): void {
  if (animal instanceof Dog) {
    // TypeScript knows animal is Dog here
    animal.bark();
  } else {
    // TypeScript knows animal is Cat here
    animal.meow();
  }
}

const myDog = new Dog();
const myCat = new Cat();

makeSound(myDog); // "Woof!"
makeSound(myCat); // "Meow!"

// instanceof with built-in types
function processInput(input: Date | string): string {
  if (input instanceof Date) {
    // TypeScript knows input is Date here
    return input.toISOString();
  } else {
    // TypeScript knows input is string here
    return input.toUpperCase();
  }
}

console.log(processInput(new Date()));
console.log(processInput('hello'));

// instanceof with Error handling
function handleError(error: Error | string): void {
  if (error instanceof Error) {
    console.error(`Error: ${error.message}`);
    console.error(`Stack: ${error.stack}`);
  } else {
    console.error(`Error: ${error}`);
  }
}

handleError(new Error('Something went wrong'));
handleError('Simple error message');

Type Guards - in Operator

The in operator checks if a property exists in an object:

in Operator Type Guard:

// Types with different properties
type Fish = {
  swim: () => void;
  fins: number;
};

type Bird = {
  fly: () => void;
  wings: number;
};

// Function using 'in' operator
function move(animal: Fish | Bird): void {
  if ('swim' in animal) {
    // TypeScript knows animal is Fish here
    console.log(`Swimming with ${animal.fins} fins`);
    animal.swim();
  } else {
    // TypeScript knows animal is Bird here
    console.log(`Flying with ${animal.wings} wings`);
    animal.fly();
  }
}

const fish: Fish = {
  swim: () => console.log('Swimming...'),
  fins: 4
};

const bird: Bird = {
  fly: () => console.log('Flying...'),
  wings: 2
};

move(fish); // "Swimming with 4 fins", "Swimming..."
move(bird); // "Flying with 2 wings", "Flying..."

// 'in' with optional properties
type UserProfile = {
  name: string;
  email: string;
  phone?: string;
  address?: {
    city: string;
    country: string;
  };
};

function displayContact(profile: UserProfile): void {
  console.log(`Name: ${profile.name}`);
  console.log(`Email: ${profile.email}`);

  if ('phone' in profile && profile.phone) {
    console.log(`Phone: ${profile.phone}`);
  }

  if ('address' in profile && profile.address) {
    console.log(`City: ${profile.address.city}`);
  }
}
Tip: The in operator is perfect for discriminating between object types based on their properties. It's especially useful when working with objects from external APIs.

Custom Type Guards (Type Predicates)

Custom type guards use type predicates (is keyword) to create reusable type-checking functions:

Type Predicate Functions:

// Custom type guard function
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

// Usage
function processValue(value: unknown): void {
  if (isString(value)) {
    // TypeScript knows value is string here
    console.log(value.toUpperCase());
  } else {
    console.log('Not a string');
  }
}

// More complex type guard
interface User {
  id: string;
  name: string;
  email: string;
}

interface Admin {
  id: string;
  name: string;
  email: string;
  permissions: string[];
}

// Type guard to check if User is Admin
function isAdmin(user: User | Admin): user is Admin {
  return 'permissions' in user;
}

function grantAccess(user: User | Admin): void {
  if (isAdmin(user)) {
    // TypeScript knows user is Admin here
    console.log(`Admin with ${user.permissions.length} permissions`);
  } else {
    // TypeScript knows user is User here
    console.log(`Regular user: ${user.name}`);
  }
}

// Array type guard
function isStringArray(value: unknown): value is string[] {
  return (
    Array.isArray(value) &&
    value.every(item => typeof item === 'string')
  );
}

function processArray(value: unknown): void {
  if (isStringArray(value)) {
    // TypeScript knows value is string[]
    value.forEach(str => console.log(str.toUpperCase()));
  } else {
    console.log('Not a string array');
  }
}

// Nullable type guard
function isDefined<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

// Filter array removing null/undefined
const values: (string | null | undefined)[] = ['a', null, 'b', undefined, 'c'];
const definedValues = values.filter(isDefined);
// Type is string[] (null and undefined removed)
console.log(definedValues); // ['a', 'b', 'c']
Note: Type predicates use the syntax parameterName is Type in the return type. The function must return a boolean, and TypeScript will narrow the type accordingly.

Discriminated Unions with Type Guards

Type guards work exceptionally well with discriminated unions:

Discriminated Unions:

// Discriminated union
type SuccessResponse = {
  status: 'success';
  data: {
    id: string;
    name: string;
  };
};

type ErrorResponse = {
  status: 'error';
  error: {
    code: string;
    message: string;
  };
};

type ApiResponse = SuccessResponse | ErrorResponse;

// Type guard using discriminant
function isSuccessResponse(
  response: ApiResponse
): response is SuccessResponse {
  return response.status === 'success';
}

function isErrorResponse(
  response: ApiResponse
): response is ErrorResponse {
  return response.status === 'error';
}

// Using type guards
function handleResponse(response: ApiResponse): void {
  if (isSuccessResponse(response)) {
    // TypeScript knows response is SuccessResponse
    console.log(`Success: ${response.data.name}`);
  } else if (isErrorResponse(response)) {
    // TypeScript knows response is ErrorResponse
    console.error(`Error ${response.error.code}: ${response.error.message}`);
  }
}

// Alternative: using discriminant directly in switch
function handleResponseSwitch(response: ApiResponse): void {
  switch (response.status) {
    case 'success':
      console.log(`Success: ${response.data.name}`);
      break;
    case 'error':
      console.error(`Error: ${response.error.message}`);
      break;
  }
}

Combining Type Guards

You can combine multiple type guards for complex type narrowing:

Combining Type Guards:

// Complex type structure
type User = {
  type: 'user';
  name: string;
  email: string;
};

type Admin = {
  type: 'admin';
  name: string;
  email: string;
  permissions: string[];
};

type Guest = {
  type: 'guest';
  sessionId: string;
};

type Account = User | Admin | Guest;

// Multiple type guards
function hasEmail(account: Account): account is User | Admin {
  return account.type === 'user' || account.type === 'admin';
}

function isAdmin(account: Account): account is Admin {
  return account.type === 'admin';
}

// Using combined type guards
function processAccount(account: Account): void {
  if (hasEmail(account)) {
    // account is User | Admin (both have email)
    console.log(`Email: ${account.email}`);

    if (isAdmin(account)) {
      // account is Admin
      console.log(`Permissions: ${account.permissions.join(', ')}`);
    }
  } else {
    // account is Guest
    console.log(`Guest session: ${account.sessionId}`);
  }
}

// Nested type narrowing
function getDisplayName(account: Account): string {
  if ('name' in account) {
    // account is User | Admin
    return account.name;
  } else {
    // account is Guest
    return `Guest (${account.sessionId})`;
  }
}

Type Guards with Arrays

Type guards are useful for filtering and processing arrays:

Array Type Guards:

// Mixed array type
type Item = string | number | boolean | null;

const items: Item[] = ['hello', 42, true, null, 'world', 0, false];

// Filter strings
const strings = items.filter((item): item is string => {
  return typeof item === 'string';
});
console.log(strings); // ['hello', 'world']

// Filter numbers (excluding zero if needed)
const numbers = items.filter((item): item is number => {
  return typeof item === 'number' && item !== 0;
});
console.log(numbers); // [42]

// Filter non-null values
const nonNullItems = items.filter((item): item is string | number | boolean => {
  return item !== null;
});
console.log(nonNullItems); // All except null

// Complex object array
type Product = {
  id: string;
  name: string;
  price: number;
  discount?: number;
};

const products: Product[] = [
  { id: '1', name: 'Laptop', price: 999, discount: 0.1 },
  { id: '2', name: 'Mouse', price: 29 },
  { id: '3', name: 'Keyboard', price: 79, discount: 0.15 }
];

// Filter products with discounts
function hasDiscount(product: Product): product is Product & { discount: number } {
  return product.discount !== undefined && product.discount > 0;
}

const discountedProducts = products.filter(hasDiscount);
discountedProducts.forEach(product => {
  // TypeScript knows discount is defined
  console.log(`${product.name}: ${product.discount * 100}% off`);
});
Exercise:
  1. Create a discriminated union Shape with three types: Circle (kind: 'circle', radius: number), Rectangle (kind: 'rectangle', width: number, height: number), and Square (kind: 'square', size: number)
  2. Create custom type guard functions isCircle, isRectangle, and isSquare for each shape type
  3. Create a function calculateArea that uses type guards to calculate the area of any shape
  4. Create a function isLargeShape that returns true if the shape's area is greater than 100
  5. Test your functions with an array of mixed shapes and filter the large ones

Summary

  • Type assertions use as keyword to tell TypeScript about a value's type
  • Non-null assertion (!) asserts a value is not null or undefined
  • typeof type guard checks primitive types at runtime
  • instanceof type guard checks if an object is an instance of a class
  • in operator type guard checks if a property exists in an object
  • Custom type guards use type predicates (is) for reusable type checks
  • Type guards enable safe type narrowing based on runtime checks
  • Combine type guards for complex type narrowing scenarios
  • Type assertions bypass checks; type guards provide safe narrowing