Schema Definition with Decorators

The @type() decorator style is the classic way of defining schema structures, and remains fully supported. The schema builder (schema() + t.*) is the recommended style for new code, but both produce the identical wire format and interoperate freely: t.array(DecoratorClass) and @type(BuilderClass) both work.

Schema examples across the docs offer a Decorators tab, and your tab selection is remembered across pages. This page collects the decorator-specific setup and details in one place.

TypeScript Config

The decorator style requires two settings in your tsconfig.json. Enable experimentalDecorators and disable useDefineForClassFields:

tsconfig.json
{
    "compilerOptions": {
        "experimentalDecorators": true,
        "useDefineForClassFields": false
    }
}

These flags are needed only for decorators. The schema() builder works without any compiler configuration. (See #510 for the discussion behind useDefineForClassFields.)

What is @type()?

@type() uses an upcoming JavaScript feature that is yet to be formally established by TC39. type is actually just a function imported from the @colyseus/schema module. By calling type with the @ prefix at the property level means we’re calling it as a property decorator. See the decorators proposal here.

Defining fields

Extend the Schema class and decorate each synchronizable field with @type(), passing one of the primitive type strings:

MyState.ts
import { Schema, type } from "@colyseus/schema";
 
export class MyState extends Schema {
    @type("string") currentTurn: string;
}

Child schemas

Pass the class itself to @type() to nest one schema inside another:

MyState.ts
import { Schema, type } from "@colyseus/schema";
 
class World extends Schema {
    @type("number") width: number;
    @type("number") height: number;
    @type("number") items: number = 10;
}
 
class MyState extends Schema {
    @type(World) world: World = new World();
}

Collections

Collection fields take the child type wrapped in the collection shape: [...] for arrays, { map: ... }, { set: ... }, or { collection: ... }. Initialize each field with its collection instance:

MyState.ts
import { Schema, ArraySchema, MapSchema, SetSchema, CollectionSchema, type } from "@colyseus/schema";
 
class Block extends Schema {
    @type("number") x: number;
    @type("number") y: number;
}
 
class Player extends Schema {
    @type([ "string" ]) animals = new ArraySchema<string>();
    @type([ Block ]) blocks = new ArraySchema<Block>();
    @type({ map: Block }) blocksByName = new MapSchema<Block>();
    @type({ set: Block }) effects = new SetSchema<Block>();
    @type({ collection: Block }) items = new CollectionSchema<Block>();
}

The collection instances themselves work exactly as documented in the main reference. Only the field declaration differs between the two styles.

Default values

Use regular class property initializers:

MyState.ts
import { Schema, MapSchema, type } from "@colyseus/schema";
 
class MyState extends Schema {
    @type("number") countdown: number = 60;
    @type({ map: Player }) players = new MapSchema<Player>();
}

Inheritance

Subclasses may define their own serialized fields, and collections accept inherited types of the same base:

MyState.ts
import { Schema, MapSchema, type, entity } from "@colyseus/schema";
 
class Item extends Schema {/* base Item fields */}
 
class Weapon extends Item {
    @type("number") damage: number;
}
 
// A subclass that adds NO new @type() fields must be registered
// explicitly with @entity: field decorators are what register a
// class with the serializer, and this class has none of its own.
@entity
class Shield extends Item {}
 
class Inventory extends Schema {
    @type({ map: Item }) items = new MapSchema<Item>();
}
 
const inventory = new Inventory();
inventory.items.set("left", new Weapon());
inventory.items.set("right", new Shield());

View tags

Fields visible only to selected clients take the @view() decorator alongside @type():

MyState.ts
import { Schema, type, view } from "@colyseus/schema";
 
class Player extends Schema {
    // visible to all
    @type("string") name: string;
 
    // any `view.add(player)` will see this field
    @view() @type("number") health: number;
 
    // only `view.add(player, 1)` will see this field
    @view(1) @type("number") position: number;
}

See State View for how StateView works. Everything there applies equally to both definition styles.

Delivery modifiers

@unreliable, @patchOnly, and @fullStateOnly control when and how a field reaches clients. Apply them alongside @type():

MyState.ts
import { Schema, type, unreliable, patchOnly, fullStateOnly } from "@colyseus/schema";
 
class Player extends Schema {
    // tick patches ride the unreliable channel (WebTransport only)
    @unreliable @type("number") aimYaw: number;
 
    // never in a full state sync: invisible to late joiners
    @patchOnly @type("number") flashIntensity: number;
 
    // full state sync only, never a tick patch
    @fullStateOnly @type("uint16") spawnIndex: number;
}

@patchOnly and @fullStateOnly are mutually exclusive: combining them throws at decoration time. Streaming collections use the type descriptor instead: @type({ stream: Entity }), with an optional priority key.

The semantics, use cases and cautions are documented in the main reference → Delivery modifiers. That includes the transport requirement for @unreliable, which is experimental and reaches clients only over WebTransport.

Deprecating fields

Flag a retired field with @deprecated() while keeping its slot in the encoding order:

MyState.ts
import { Schema, type, deprecated } from "@colyseus/schema";
 
class MyState extends Schema {
    @deprecated() @type("string") myField: string;
 
    // new fields always at the end of the structure
    @type("string") newField: string;
}

See Versioning for the full backwards/forwards compatibility workflow.

Legacy: defineTypes()

⚠️

defineTypes() is deprecated: it still works, but logs a deprecation warning at runtime and in schema-codegen. Migrate to schema() with t.* builders. (It was briefly removed in early schema 5 releases, 5.0.0–5.0.8. Update @colyseus/schema if you hit a missing-export error.)

MyState.js
import { Schema, defineTypes } from "@colyseus/schema";
 
class MyState extends Schema {
}
defineTypes(MyState, {
    currentTurn: "string",
});

defineTypes() delegates to the same pipeline as the @type() decorator, so unlike schema() it still accepts raw type strings ("string", "number") as field values.

See the 0.18 migration guide for upgrade steps.