DevToolBoxGRATIS
Blogg

TypeScript Type Guards: Komplett Guide til Runtime-typekontroll

13 minby DevToolBox

TypeScript provides a powerful type system, but types are erased at runtime. Type guards bridge this gap by allowing you to narrow types at runtime while keeping full type safety at compile time. This comprehensive guide covers every type guard technique, from basic typeof checks to advanced discriminated unions and assertion functions, with practical examples you can use in production code.

What Are Type Guards?

A type guard is a runtime check that narrows the type of a variable within a conditional block. TypeScript understands these checks and refines the type automatically, giving you precise autocompletion and type checking inside the guarded block.

// Without type guard — TypeScript only knows 'value' is string | number
function process(value: string | number) {
  // value.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'number'

  // With type guard — TypeScript narrows the type
  if (typeof value === 'string') {
    console.log(value.toUpperCase()); // OK: value is string here
  } else {
    console.log(value.toFixed(2));    // OK: value is number here
  }
}

typeof Type Guards

The typeof operator is the simplest type guard. It checks the JavaScript runtime type and TypeScript narrows the type accordingly. typeof works for primitive types: string, number, bigint, boolean, symbol, undefined, object, and function.

function formatValue(value: string | number | boolean): string {
  if (typeof value === 'string') {
    // TypeScript knows: value is string
    return value.trim().toLowerCase();
  }
  if (typeof value === 'number') {
    // TypeScript knows: value is number
    return value.toLocaleString('en-US', { maximumFractionDigits: 2 });
  }
  // TypeScript knows: value is boolean
  return value ? 'Yes' : 'No';
}

// typeof works for these primitive types:
// 'string' | 'number' | 'bigint' | 'boolean'
// 'symbol' | 'undefined' | 'object' | 'function'

// Caveat: typeof null === 'object'
function processNullable(value: string | null) {
  if (typeof value === 'object') {
    // value could be null here!  typeof null === 'object'
  }
  if (value !== null && typeof value === 'object') {
    // Safe: value is not null
  }
}

instanceof Type Guards

The instanceof operator checks whether an object is an instance of a class. TypeScript narrows the type to the class within the truthy branch. This is particularly useful for error handling and working with class hierarchies.

class HttpError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = 'HttpError';
  }
}

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

function handleError(error: Error) {
  if (error instanceof HttpError) {
    // TypeScript knows: error is HttpError
    console.log(`HTTP ${error.statusCode}: ${error.message}`);
    if (error.statusCode === 401) {
      redirectToLogin();
    }
  } else if (error instanceof ValidationError) {
    // TypeScript knows: error is ValidationError
    console.log(`Validation failed on field: ${error.field}`);
    highlightField(error.field);
  } else {
    // TypeScript knows: error is Error
    console.log(`Unexpected error: ${error.message}`);
  }
}

// instanceof with Date
function formatDate(input: string | Date): string {
  if (input instanceof Date) {
    return input.toISOString();
  }
  return new Date(input).toISOString();
}

The "in" Operator Type Guard

The in operator checks whether a property exists on an object. TypeScript narrows the type based on which properties are present. This is especially useful for discriminating between object types that share some but not all properties.

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function move(animal: Bird | Fish) {
  if ('fly' in animal) {
    // TypeScript knows: animal is Bird
    animal.fly();
  } else {
    // TypeScript knows: animal is Fish
    animal.swim();
  }
}

// Practical example: API responses
interface SuccessResponse {
  data: unknown;
  status: 'ok';
}

interface ErrorResponse {
  error: string;
  code: number;
}

function handleResponse(res: SuccessResponse | ErrorResponse) {
  if ('error' in res) {
    // TypeScript knows: res is ErrorResponse
    console.error(`Error ${res.code}: ${res.error}`);
  } else {
    // TypeScript knows: res is SuccessResponse
    processData(res.data);
  }
}

Custom Type Guard Functions

Custom type guards are functions that return a type predicate (paramName is Type). They let you encapsulate complex type-checking logic in reusable functions. The compiler trusts your assertion, so ensure the check is correct.

// Type predicate syntax: paramName is Type
interface User {
  id: string;
  name: string;
  email: string;
}

interface Admin extends User {
  role: 'admin';
  permissions: string[];
}

// Custom type guard function
function isAdmin(user: User): user is Admin {
  return 'role' in user && (user as Admin).role === 'admin';
}

function showDashboard(user: User) {
  if (isAdmin(user)) {
    // TypeScript knows: user is Admin
    console.log(`Admin: ${user.name}, Permissions: ${user.permissions.join(', ')}`);
    renderAdminPanel(user.permissions);
  } else {
    // TypeScript knows: user is User (not Admin)
    console.log(`User: ${user.name}`);
    renderUserDashboard();
  }
}

// Type guard for checking non-null
function isNotNull<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

// Use with .filter() to narrow array types
const items: (string | null)[] = ['hello', null, 'world', null];
const strings: string[] = items.filter(isNotNull);
// strings is string[] — no more nulls!

// Type guard for object shapes
function isValidUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data &&
    'email' in data &&
    typeof (data as User).id === 'string' &&
    typeof (data as User).name === 'string' &&
    typeof (data as User).email === 'string'
  );
}

Discriminated Unions

Discriminated unions (also called tagged unions) use a common literal property to distinguish between union members. TypeScript narrows the type when you check the discriminant property. This is one of the most powerful and type-safe patterns in TypeScript.

// The discriminant property: 'type'
interface Circle {
  type: 'circle';
  radius: number;
}

interface Rectangle {
  type: 'rectangle';
  width: number;
  height: number;
}

interface Triangle {
  type: 'triangle';
  base: number;
  height: number;
}

type Shape = Circle | Rectangle | Triangle;

function calculateArea(shape: Shape): number {
  switch (shape.type) {
    case 'circle':
      // TypeScript knows: shape is Circle
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      // TypeScript knows: shape is Rectangle
      return shape.width * shape.height;
    case 'triangle':
      // TypeScript knows: shape is Triangle
      return (shape.base * shape.height) / 2;
  }
}

// Real-world: Redux actions
type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: User[] }
  | { type: 'FETCH_ERROR'; error: string }
  | { type: 'SET_FILTER'; filter: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true };
    case 'FETCH_SUCCESS':
      // TypeScript knows: action.payload exists and is User[]
      return { ...state, loading: false, users: action.payload };
    case 'FETCH_ERROR':
      // TypeScript knows: action.error exists and is string
      return { ...state, loading: false, error: action.error };
    case 'SET_FILTER':
      return { ...state, filter: action.filter };
  }
}

Assertion Functions

Assertion functions narrow types by throwing an error if the assertion fails. Unlike type predicates that return a boolean, assertion functions use asserts paramName is Type to indicate they throw rather than return false. This pattern is ideal for validation at the boundaries of your application.

// Assertion function syntax: asserts paramName is Type
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new TypeError(`Expected string, got ${typeof value}`);
  }
}

// Usage: type narrows AFTER the assertion call
function processInput(input: unknown) {
  assertIsString(input);
  // TypeScript knows: input is string from here on
  console.log(input.toUpperCase());
}

// Assert non-null
function assertDefined<T>(
  value: T | null | undefined,
  name: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(`${name} must be defined`);
  }
}

// Practical: Form validation
interface FormData {
  name: unknown;
  email: unknown;
  age: unknown;
}

function assertValidForm(data: FormData): asserts data is {
  name: string;
  email: string;
  age: number;
} {
  if (typeof data.name !== 'string' || data.name.length === 0) {
    throw new ValidationError('name', 'Name is required');
  }
  if (typeof data.email !== 'string' || !data.email.includes('@')) {
    throw new ValidationError('email', 'Valid email is required');
  }
  if (typeof data.age !== 'number' || data.age < 0 || data.age > 150) {
    throw new ValidationError('age', 'Valid age is required');
  }
}

function handleSubmit(data: FormData) {
  assertValidForm(data);
  // TypeScript knows all fields are properly typed here
  console.log(`Name: ${data.name}, Email: ${data.email}, Age: ${data.age}`);
}

Type Narrowing with Control Flow

TypeScript performs control flow analysis to narrow types based on assignments, equality checks, and truthiness. Understanding how TypeScript narrows types helps you write cleaner code without explicit type guards.

Truthiness Narrowing

TypeScript narrows types based on truthiness checks, filtering out null, undefined, 0, empty strings, and false.

function greet(name: string | null | undefined) {
  if (name) {
    // TypeScript knows: name is string (not null/undefined)
    console.log(`Hello, ${name.toUpperCase()}!`);
  }
}

// Truthy narrowing with arrays
function processItems(items?: string[]) {
  if (items && items.length > 0) {
    // TypeScript knows: items is string[] (defined and non-empty)
    const first = items[0]; // string
  }
}

// Logical operators for narrowing
function getValue(a: string | null, b: string | null): string {
  // || narrows: returns first truthy value
  return a || b || 'default';
}

// Nullish coalescing for narrowing
function getConfig(config?: { timeout: number }) {
  const timeout = config?.timeout ?? 3000;
  // timeout is number (never undefined)
}

Equality Narrowing

TypeScript narrows types when you compare values with ===, !==, ==, or != operators.

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // TypeScript knows: both x and y are string
    // (the only common type)
    console.log(x.toUpperCase());
    console.log(y.toUpperCase());
  }
}

// Equality narrowing with null
function process(value: string | null) {
  if (value !== null) {
    // TypeScript knows: value is string
    console.log(value.length);
  }
}

// Switch statement narrowing
function handleStatus(status: 'loading' | 'success' | 'error') {
  switch (status) {
    case 'loading':
      showSpinner();
      break;
    case 'success':
      hideSpinner();
      break;
    case 'error':
      showError();
      break;
  }
}

Advanced Type Guard Patterns

Exhaustive Checks with never

Use the never type to ensure all union cases are handled. If you add a new case to the union, TypeScript will error at the exhaustive check location, forcing you to handle it.

type Shape = Circle | Rectangle | Triangle;

function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

function getArea(shape: Shape): number {
  switch (shape.type) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      // If you add a new Shape variant and forget to handle it,
      // TypeScript will error here because shape won't be 'never'
      return assertNever(shape);
  }
}

// Alternative: satisfies + exhaustive check
function describeShape(shape: Shape): string {
  switch (shape.type) {
    case 'circle':    return `Circle with radius ${shape.radius}`;
    case 'rectangle': return `${shape.width}x${shape.height} rectangle`;
    case 'triangle':  return `Triangle with base ${shape.base}`;
    default: {
      const _exhaustive: never = shape;
      return _exhaustive;
    }
  }
}

Generic Type Guards

You can create generic type guards that work with any type, making your type-checking utilities reusable across your codebase.

// Generic type guard for checking object keys
function hasProperty<K extends string>(
  obj: unknown,
  key: K
): obj is Record<K, unknown> {
  return typeof obj === 'object' && obj !== null && key in obj;
}

// Usage
function processData(data: unknown) {
  if (hasProperty(data, 'name') && hasProperty(data, 'age')) {
    console.log(data.name, data.age); // both are 'unknown' but accessible
  }
}

// Generic type guard for arrays
function isArrayOf<T>(
  arr: unknown,
  guard: (item: unknown) => item is T
): arr is T[] {
  return Array.isArray(arr) && arr.every(guard);
}

const isString = (value: unknown): value is string =>
  typeof value === 'string';

function handleInput(data: unknown) {
  if (isArrayOf(data, isString)) {
    // data is string[]
    data.forEach(s => console.log(s.toUpperCase()));
  }
}

Array Type Guards

TypeScript provides special narrowing for arrays. Use Array.isArray() and type-safe filter patterns to work with arrays of union types.

// Filtering with type guards
const mixed: (string | number | null)[] = ['a', 1, null, 'b', 2, null];

// Filter nulls with type predicate
const nonNull = mixed.filter(
  (item): item is string | number => item !== null
);
// nonNull: (string | number)[]

// Filter to specific type
const stringsOnly = mixed.filter(
  (item): item is string => typeof item === 'string'
);
// stringsOnly: string[]

// Type-safe .find()
const found = mixed.find(
  (item): item is string => typeof item === 'string'
);
// found: string | undefined

// Array.isArray() type guard
function flatten(input: string | string[]): string[] {
  if (Array.isArray(input)) {
    return input; // string[]
  }
  return [input]; // wrap single string in array
}

Real-World Examples

API Response Handling

Type guards are essential for validating API responses at runtime, ensuring type safety when dealing with external data.

// API response types
interface ApiSuccess<T> {
  success: true;
  data: T;
}

interface ApiError {
  success: false;
  error: { message: string; code: string };
}

type ApiResponse<T> = ApiSuccess<T> | ApiError;

// Type guard based on discriminant
function isApiSuccess<T>(res: ApiResponse<T>): res is ApiSuccess<T> {
  return res.success === true;
}

// Usage with fetch
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const json: ApiResponse<User> = await response.json();

  if (isApiSuccess(json)) {
    return json.data; // TypeScript knows: json.data is User
  } else {
    throw new Error(json.error.message);
  }
}

// Validate unknown API data
function isUser(data: unknown): data is User {
  if (typeof data !== 'object' || data === null) return false;
  const obj = data as Record<string, unknown>;
  return (
    typeof obj.id === 'string' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string'
  );
}

Event Handling

Use type guards to narrow DOM event types for type-safe event handling.

function handleEvent(event: Event) {
  if (event instanceof MouseEvent) {
    console.log(`Mouse at (${event.clientX}, ${event.clientY})`);
  } else if (event instanceof KeyboardEvent) {
    console.log(`Key pressed: ${event.key}`);
  } else if (event instanceof TouchEvent) {
    console.log(`Touch points: ${event.touches.length}`);
  }
}

// Input element type narrowing
function handleInput(event: Event) {
  const target = event.target;
  if (target instanceof HTMLInputElement) {
    console.log(target.value); // string
  } else if (target instanceof HTMLSelectElement) {
    console.log(target.selectedIndex); // number
  }
}

Error Handling

Type guards help you handle different error types with proper type safety.

// Type guard for unknown caught errors
function isError(error: unknown): error is Error {
  return error instanceof Error;
}

function getErrorMessage(error: unknown): string {
  if (isError(error)) return error.message;
  if (typeof error === 'string') return error;
  return 'An unknown error occurred';
}

// Safe error handling in async code
async function safeFetch(url: string) {
  try {
    const res = await fetch(url);
    if (!res.ok) throw new HttpError(res.status, res.statusText);
    return await res.json();
  } catch (error) {
    if (error instanceof HttpError) {
      handleHttpError(error); // error.statusCode available
    } else if (error instanceof TypeError) {
      handleNetworkError(error); // Network failure
    } else {
      handleUnknownError(getErrorMessage(error));
    }
  }
}

Best Practices

  1. Prefer discriminated unions over type predicates when possible — they are inherently type-safe.
  2. Keep type guard functions simple and focused on a single type check.
  3. Use assertion functions for validation at application boundaries (API handlers, form submissions).
  4. Always handle the else case or use exhaustive checking with never.
  5. Use Array.isArray() before accessing array methods on union types.
  6. Combine multiple type guards with logical operators for complex narrowing.
  7. Test your custom type guards thoroughly — TypeScript trusts your predicate.

Conclusion

TypeScript type guards are essential tools for bridging the gap between compile-time type safety and runtime behavior. Start with simple typeof and instanceof guards for primitive and class types, use the in operator and discriminated unions for object types, and create custom type guard functions for complex validation logic. Assertion functions are powerful for input validation at application boundaries. By mastering these patterns, you can write TypeScript code that is both type-safe at compile time and robust at runtime, eliminating entire categories of bugs from your applications.

FAQ

What is the difference between a type predicate and an assertion function?

A type predicate (param is Type) returns a boolean and narrows the type in the truthy branch. An assertion function (asserts param is Type) narrows the type after the function call by throwing an error if the assertion fails, rather than returning false.

When should I use discriminated unions vs custom type guards?

Use discriminated unions when you control the types and can add a discriminant property. They are inherently type-safe because TypeScript checks them structurally. Use custom type guards when working with external data or types you do not control.

Can type guards cause runtime errors?

Custom type guard functions can be incorrect if the predicate logic does not match the declared type. TypeScript trusts your assertion, so if your check is wrong, you can get runtime errors. Always test type guards thoroughly.

How do I narrow types in arrays?

Use Array.isArray() to narrow to an array type. Use .filter() with a type predicate to narrow array element types. For example: arr.filter((x): x is string => typeof x === "string") returns a string[] from a mixed array.

𝕏 Twitterin LinkedIn
Var dette nyttig?

Hold deg oppdatert

Få ukentlige dev-tips og nye verktøy.

Ingen spam. Avslutt når som helst.

Try These Related Tools

{ }JSON FormatterJSTypeScript to JavaScript

Related Articles

TypeScript Generics Komplett Guide 2026: Fra Grunnleggende til Avanserte Monstre

Mestr TypeScript generics: typeparametere, constraints, betingede typer, mapped types, utility types og praktiske monstre.

TypeScript Utility Types Jukseark: Partial, Pick, Omit og mer

Komplett referanse for TypeScript utility types med praktiske eksempler. Partial, Required, Pick, Omit, Record, Exclude, Extract, ReturnType og avanserte monstre.

TypeScript vs JavaScript: Naar bruke hva

Praktisk sammenligning av TypeScript og JavaScript. Typesikkerhet, kodeeksempler, migrasjonsstrategier, ytelse, oekosystem og beslutningsguide.