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.
Method 1: TypeScript Declaration Merging (Recommended)
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:
interface Body: AddsuserDatato instances ofMatter.Body.interface IBodyDefinition: Allows you to passuserDatadirectly into creation methods such asBodies.rectangleorBody.createwithout triggering excess property check errors.
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.