Ensuring Type Safety at Runtime: Effective Techniques for Checking Custom Types in TypeScript

2024-07-27

  • In TypeScript, typeof primarily acts as the JavaScript typeof operator during runtime. It returns a string representing the underlying data type of a value: "string", "number", "boolean", "object", etc.
  • However, TypeScript also leverages typeof in a type guard context. Here, it's used to narrow down the possible types that a variable might hold based on the runtime type it evaluates to.

Why typeof Doesn't Directly Check Custom Types

  • TypeScript's type system is for static type checking, which happens at compile time. During compilation, type annotations, interfaces, and other type constructs are erased.
  • Since typeof operates at runtime, it can't directly access TypeScript's type information that's no longer present.

Alternative: Type Guards for Runtime Type Checking

To check if a value conforms to a custom type at runtime, TypeScript employs type guards. These are functions that return a boolean indicating whether the input value matches the expected type. Here's a common approach:

interface Point {
  x: number;
  y: number;
}

function isPoint(value: any): value is Point {
  return typeof value === 'object' && 'x' in value && 'y' in value;
}

const someValue: any = { x: 10, y: 20 };

if (isPoint(someValue)) {
  // Now you can safely access someValue.x and someValue.y
  console.log(someValue.x, someValue.y);
}

In this example:

  1. We define a Point interface to describe the expected structure of a point object.
  2. The isPoint function acts as a type guard. It checks if the input value is an object and has the required x and y properties.
  3. Inside the if statement, the type guard ensures that someValue is narrowed to the Point type if the condition is true. This allows safe access to its properties.

Key Points:

  • typeof on its own can't directly check for custom types in TypeScript.
  • Use type guards like isPoint to perform runtime type checks and narrow down possible types.
  • Type guards are crucial for working with dynamic values (like any) and ensuring type safety at runtime.



const fruit = ['apple', 'orange', 'banana'] as const; // `as const` asserts the array elements are literals
type Fruit = typeof fruit[number]; // Type inference from literal array

let myFruit: Fruit = 'apple'; // Allowed
// myFruit = 'grape'; // Error: 'grape' is not a valid Fruit

if (myFruit === 'apple') {
  console.log(`${myFruit} is a valid fruit.`);
}
  • We create a constant array fruit with literal string values.
  • Using as const, we tell TypeScript to preserve these values as literals.
  • The typeof fruit[number] expression infers a type that's a union of the literal string types in the array ("apple" | "orange" | "banana").
  • This inferred type is assigned to Fruit, allowing us to restrict myFruit to only those specific fruit names.

User-Defined Type Guard Function (for more complex type checks):

interface Product {
  name: string;
  price: number;
  stock?: number; // Optional property
}

function isProduct(value: any): value is Product {
  return typeof value === 'object' &&
         'name' in value && typeof value.name === 'string' &&
         'price' in value && typeof value.price === 'number' &&
         // Optional property check (if needed)
         (typeof value.stock === 'undefined' || typeof value.stock === 'number');
}

const someData: any = { name: 'T-Shirt', price: 19.99, stock: 10 }; // Matches Product

if (isProduct(someData)) {
  console.log(`Product details: ${someData.name}, Price: $${someData.price}`);
} else {
  console.error('Invalid product data format.');
}

Here's a breakdown:

  • The isProduct function acts as a type guard, checking for required properties and their types.
  • Inside the if statement, the type guard ensures that someData is narrowed to the Product type if the condition is true.
  • We can then safely access its properties like name and price.

Choosing the Right Approach:

  • Use type inference with literal types when you have a fixed set of known values and want to restrict the type based on those literals.
  • Use user-defined type guards for more complex type checks, especially when dealing with dynamic or user-provided data.



  • The in operator can be used to check if a specific property exists on an object, but it doesn't guarantee the property's type.
  • While not a complete type check, it can be helpful in certain scenarios.
interface User {
  name: string;
  age: number;
}

function hasRequiredProperties(obj: any): obj is User {
  return 'name' in obj && 'age' in obj;
}

const userData: any = { name: 'Alice', age: 30 };

if (hasRequiredProperties(userData)) {
  // userData might still have other properties, so be cautious
  console.log(`User: ${userData.name} (age: ${userData.age})`);
} else {
  console.error('Missing required user properties.');
}

instanceof (for classes):

  • The instanceof operator can be used with classes to check if an object was created from that class. However, it only works for classes, not interfaces.
class Animal {
  constructor(public type: string) {}
}

class Dog extends Animal {
  constructor() {
    super('dog');
  }
}

const unknownAnimal: Animal = new Dog();

if (unknownAnimal instanceof Dog) {
  console.log(`It's a dog!`);
} else if (unknownAnimal instanceof Animal) {
  console.log(`It's an animal, but not necessarily a dog.`);
} else {
  console.error('Unexpected type for unknownAnimal.');
}

Custom Type Guards with Discriminated Unions (for more control):

  • Discriminated unions involve interfaces or classes that share a common property with a specific type value. This property helps differentiate between types within the union.
  • You can write type guards that check for the presence and value of this discriminator property.
interface Shape {
  type: 'circle' | 'rectangle';
  radius?: number; // Only applicable for circles
  width?: number;
  height?: number; // Only applicable for rectangles
}

function getShapeArea(shape: Shape): number {
  if (shape.type === 'circle') {
    if (shape.radius === undefined) {
      throw new Error('Circle requires a radius property.');
    }
    return Math.PI * Math.pow(shape.radius, 2);
  } else if (shape.type === 'rectangle') {
    if (shape.width === undefined || shape.height === undefined) {
      throw new Error('Rectangle requires both width and height properties.');
    }
    return shape.width * shape.height;
  } else {
    throw new Error('Invalid shape type.');
  }
}

const circle: Shape = { type: 'circle', radius: 5 };
const rectangle: Shape = { type: 'rectangle', width: 10, height: 2 };

console.log(`Circle area: ${getShapeArea(circle)}`);
console.log(`Rectangle area: ${getShapeArea(rectangle)}`);
  • Use in for quick checks of property existence (but be cautious).
  • Use instanceof only when dealing with classes.
  • Use discriminated unions and custom type guards for more control and complex type checks with clear type differentiation.

typescript types



Understanding Dynamic Property Assignment in TypeScript

Understanding the Concept:In TypeScript, objects are collections of key-value pairs, where keys are property names and values are the corresponding data associated with those properties...


TypeScript Object Literal Types: Examples

Type Definitions in Object LiteralsIn TypeScript, object literals can be annotated with type definitions to provide more precise and informative code...


Example of Class Type Checking in TypeScript

Class Type Checking in TypeScriptIn TypeScript, class type checking ensures that objects adhere to the defined structure of a class...


Understanding Getters and Setters in TypeScript with Example Code

Getters and SettersIn TypeScript, getters and setters are special methods used to access or modify the values of class properties...


Taming Numbers: How to Ensure Integer Properties in TypeScript

Type Annotation:The most common approach is to use type annotations during class property declaration. Here, you simply specify the type of the property as number...



typescript types

Interactive Buttons vs. Form Submission: Demystifying `` and ``

HTML forms: Used to collect user input on a web page. They typically consist of various input elements like text boxes, radio buttons


Limiting File Formats with <input type="file">

Purpose:To restrict the types of files users can upload to your web application.To enhance user experience by preventing unexpected file types


Checking if a Value is an Object in JavaScript

Understanding ObjectsIn JavaScript, an object is a collection of key-value pairs. It's a fundamental data type used to store and organize data


Understanding TypeScript Constructors, Overloading, and Their Applications

Constructors are special functions in classes that are called when you create a new object of that class. They're responsible for initializing the object's properties (variables) with starting values


Setting a New Property on window in TypeScript

Direct Assignment:The most straightforward method is to directly assign a value to the new property:This approach creates a new property named myNewProperty on the window object and assigns the string "Hello