TypeScript Types for Matter.js Body Custom Data

Extending Matter.js bodies with custom user data requires augmenting the existing type declarations provided by @types/matter-js. Because Matter.js does not include a strongly typed userData field by default like some other physics engines, developers often rely on TypeScript's declaration merging or intersection types. This guide explains how to use module augmentation to seamlessly add a typed userData property to both the Body interface and its creation options, as well as an alternative approach using type intersections.

Declaration merging (module augmentation) is the cleanest way to extend Matter.js types across an entire project. It ensures that whenever you create or interact with a Matter.Body, TypeScript recognizes your custom properties without requiring explicit type casting.

Create a type declaration file (for example, matter-augment.d.ts) anywhere inside your TypeScript project's src folder:

import 'matter-js';

// Define the shape of your custom data
export interface MyCustomData {
  id: string;
  health: number;
  isCollectable: boolean;
  ownerType: 'player' | 'enemy' | 'neutral';
}

// Augment the matter-js module
declare module 'matter-js' {
  interface Body {
    userData?: MyCustomData;
  }

  interface IBodyDefinition {
    userData?: MyCustomData;
  }
}

How It Works:

Usage Example:

import { Bodies, Body } from 'matter-js';
import { MyCustomData } from './matter-augment';

// Create a body with custom user data
const playerBody = Bodies.rectangle(100, 100, 50, 50, {
  label: 'player_hitbox',
  userData: {
    id: 'p1',
    health: 100,
    isCollectable: false,
    ownerType: 'player',
  },
});

// Access the properties with full type-safety
if (playerBody.userData) {
  console.log(playerBody.userData.health); // Typed as number
}

Method 2: Intersection Types

If you prefer not to augment global definitions or if different bodies require distinct data structures, use TypeScript intersection types. This approach restricts the typing to specific instances where you apply the type alias.

import Matter from 'matter-js';

export interface EnemyData {
  enemyType: 'goblin' | 'orc';
  damage: number;
}

// Define the extended body type
export type EnemyBody = Matter.Body & {
  userData: EnemyData;
};

// Helper function to instantiate typed bodies
export function createEnemyBody(x: number, y: number, data: EnemyData): EnemyBody {
  const body = Matter.Bodies.circle(x, y, 20) as EnemyBody;
  body.userData = data;
  return body;
}

// Usage
const enemy = createEnemyBody(200, 150, {
  enemyType: 'goblin',
  damage: 15,
});

console.log(enemy.userData.damage);

Choosing the Right Approach

Use declaration merging when you want a consistent, project-wide userData container accessible on all physics bodies and event listeners (such as collisionStart). Use intersection types when you have multiple disparate entity types requiring distinct custom schemas across different modules.