npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

monomorph

v2.3.0

Published

Monomorph

Readme

npm version repo license PRs Welcome

Monomorph is a Typescript and Javascript library for performance-critical applications. Monomorph was originally developed by Hamza Kubba to power Bounce, a free & open source physics library for the web. Monomorph also powers MECS, a free & open source ECS library.

With Monomorph, you can easily create classes that:

  • have built-in object pooling to minimize garbage collection
  • can nest and/or reference monomorph classes
  • have built-in performant serialization/deserialization methods for data (including handling nesting and references), as well as other utility methods like .copy() which copies including nested object values.
  • keep more methods monomorphic, avoiding potential slowdowns of up to 60x
  • avoid repetitive boilerplate
  • are strongly typed, when using Typescript

How it works

In a nutshell, Monomorph dynamically creates the JS code for a class based on the schema you provide, then you extend that class. Each generated class has unique helper methods that are not inherited, which avoids performance drops that result from class inheritance in JS. Read "Keep in Monomorphic" for more info about monomorphism, why it matters, and how this library helps.

Install into your project

npm install monomorph

Usage

Basic class schema (JavaScript)

Let's say we want to create a Vector class, which has x and y numbers on it. This is what that would look like in

import { createClass, NumberType } from 'monomorph';

const vectorProps = {
  x: 0,
  y: 0,
  // ^^ this is shorthand for:
  // y: NumberType(0)
};

// note the double function call, the first () is required to set up generics in TypeSscript, but unfortunately still needed in JS
class Vector extends createClass()(vectorProps) {
  /* your own methods here, for example distanceTo() */
  distanceTo(otherVector) {
    const dx = this.x - otherVector.x;
    const dy = this.y - otherVector.y;
    return Math.sqrt(dx * dx + dy * dy);
  }
}
import { createClass, NumberType, props } from 'monomorph';

const vectorProps = props({
  x: 0,
  y: 0,
  // ^^ this is shorthand for:
  // y: NumberType(0)
});

// note the double function call, the first () is required to set up generics in TypeSscript
class Vector extends createClass<Vector>()(vectorProps) {
  /* your own methods here, for example distanceTo() */
  distanceTo(otherVector: Vector) {
    const dx = this.x - otherVector.x;
    const dy = this.y - otherVector.y;
    return Math.sqrt(dx * dx + dy * dy);
  }
}

Using Monomorph classes

// Vector defined above

const v1 = Vector.create(); // x and y are 0, based on the defaults defined in the schema
const v2 = Vector.create({ x: 1, y: 2 });
const v3 = Vector.create({ x: 3 }); // y is 0

v1.x = 5;
v1.reset(); // built-in method, now v1 is reset to the defaults defined in the schema (x:0, y:0)
v1.copy(v2); // built-in method, now v1 has the same values as v2, but they are still different objects

const distance = v1.distanceTo(v3); // custom method defined above

Object pooling

Monomorph automatically creates a Pool class for classes you create with it. For example, with the Vector class defined above, Vector.Pool is the pool class. This is how you use it:

// Vector defined above

// this creates a pool of vectors
const vectorPool = new Vector.Pool();

// v1 is created inside vectorPool
const v1 = Vector.create({ x: 1, y: 2 }, vectorPool);

// v2 is created in the same pool as v1
const v2 = v1.createInPool({ x: 3, y: 4 });

// alternate syntax
const v3 = vectorPool.create({ x: 5, y: 6 });

for (const vector of vectorPool) {
  // loops over v1, v2, v3
}

// v2 is returned to the pool
v2.destroy();

for (const vector of vectorPool) {
  // because we used v2.destroy(), this now loops over v1, v3
}

// creating v4 now will automatically reuse the v2 object
// however v4 will be reset to the defaults, { x: 0, y: 0 }
const v4 = vectorPool.create();

for (const vector of vectorPool) {
  // loops over v1, v4, v3
}

Nested objects (JavaScript)

Monomorph supports its classes being composed of other monomorph classes in a hierarchy. Child objects automatically get created when the parent is created or reused

import { createClass, ChildType } from 'monomorph';

// Vector as defined above in the "Basic class schema" section

const boundsProps = {
  min: Vector,
  max: Vector,
  // ^^ this is shorthand for:
  // max: ChildType(Vector),
};

class Bounds extends createClass()(boundsProps) {}

const bounds = Bounds.create({
  min: { x: -1, y: -2 },
  max: { x: 10, y: 5 },
});

// now, bounds.min and bounds.max are automatically instances of the Vector class
import { createClass, ChildType, props } from 'monomorph';

// Vector as defined above in the "Basic class schema" section

const boundsProps = props({
  min: Vector,
  max: Vector,
  // ^^ this is shorthand for:
  // max: ChildType(Vector),
});

class Bounds extends createClass<Bounds>()(boundsProps) {}

const bounds = Bounds.create({
  min: { x: -1, y: -2 },
  max: { x: 10, y: 5 },
});

// now, bounds.min and bounds.max are automatically instances of the Vector class

References (JavaScript)

To reference another monormorph instance without it being dependent on this one, there is ReferenceType() and LazyReferenceType(). They function the same in practice, however LazyReferenceType() allows you to have circular dependencies, including references to instances of the same class.

const listItemProps = {
  vector: ReferenceType(Vector),
  next: LazyReferenceType(() => ListItem),
};

class ListItem extends createClass()(listItemProps) {}

const myVector = Vector.create();
const listItem = ListItem.create({
  vector: myVector,
});

console.log(listItem.next); // null
console.log(listItem.vector === myVector); // true

const myVector2 = Vector.create({ x: 1, y: 2 });

const listItem2 = ListItem.create({
  vector: myVector2,
});

console.log(listItem2.next); // null
console.log(listItem2.vector === myVector2); // true
console.log(listItem2.vector === listItem.vector); // false

listItem.next = listItem2;
console.log(listItem.next); // listItem2
console.log(listItem.next.vector === myVector2); // true

listItem2.destroy();

// ReferenceType and LazyReferenceType automatically handle destroyed objects
// this works even if the object is reused, as reference types automatically track versioning
console.log(listItem.next); // null
const listItemProps = props({
  vector: ReferenceType(Vector),
  // unfortunately this is quite an unpleasant workaround to get proper typing for
  // lazy references, until Typescript supports recursive/circular types better
  next: LazyReferenceType((() => ListItem) as () => never) as PropertyDefinitionReference<ListItem | null, true>,
});

class ListItem extends createClass<ListItem>()(listItemProps) {}

const myVector = Vector.create();
const listItem = ListItem.create({
  vector: myVector,
});

console.log(listItem.next); // null
console.log(listItem.vector === myVector); // true

const myVector2 = Vector.create({ x: 1, y: 2 });

const listItem2 = ListItem.create({
  vector: myVector2,
});

console.log(listItem2.next); // null
console.log(listItem2.vector === myVector2); // true
console.log(listItem2.vector === listItem.vector); // false

listItem.next = listItem2;
console.log(listItem.next); // listItem2
console.log(listItem.next.vector === myVector2); // true

listItem2.destroy();

// ReferenceType and LazyReferenceType automatically handle destroyed objects
// this works even if the object is reused, as reference types automatically track versioning
console.log(listItem.next); // null

Custom Types

For various reasons, sometimes you want a field in your schema to be a non-monomorph class. For example, you might want to reference an HTML element, or use a TypedArray. This is what the CustomType is for, and it allows you to customize what happens to the field at various points in the instance's lifecycle, for example you may have some custom handling of the field when the instance is created or destroyed. Currently, the best way to see how to use custom types is to see the related tests in tests/customType.test.ts.

Serializing/Deserializing

// Bounds and Vector as defined above

const bounds = Bounds.create({
  min: { x: -1, y: -2 },
  max: { x: 10, y: 5 },
});

const array = new Float64Array(); // can also be Float32Array or a basic js Array []
bounds.toArray(array); // toArray and fromArray are automagically generated methods

const bounds2 = Bounds.create(); // create with default values
bounds2.fromArray(array); // now bounds2 has the same values as bounds

const aabb3 = Bounds.createFromArray(array); // same as bounds2 in one step

// Pools can also be serialized/deserialized this way
const vectorPool = new Vector.Pool();
const boundsPool = new Bounds.Pool();

const vectorPool2 = new Vector.Pool();
const boundsPool2 = new Bounds.Pool();

let startOffset = 0;
const array = [];
startOffset = vectorPool.toArray(array, startOffset);
startOffset = boundsPool.toArray(array, startOffset);

startOffset = 0;
startOffset = vectorPool2.fromArray(array, startOffset);
startOffset = boundsPool2.fromArray(array, startOffset);
// now, vectorPool2 has the same number of instances with the same data as vectorPool, and
// likewise for boundsPool2 copying boundsPool

// serializing and deserializing references is necessarily more complex,
// see `tests/serialization.test.ts` for in-depth examples

API summary/reference

[!NOTE]

Parameters ending with ? below means those parameters are optional, meaning they can be omitted, and they can receive undefined as a value to skip them. E.g. MyClass.create(undefined, myPool).

Classes created using Monomorph have the following static methods: | Method | Description | | --- | --- | | TheClass.create(dataObj?, pool?) | Creates an instance of this class and returns it. dataObj is, by default, expected to be a JSON representation of the expected tree of data. If a pool is provided, the instance is created inside it. Automatically reuses destroyed instances from the pool if available, calling .reset(dataObj?) on them. Returns the new instance. | | TheClass.createFromArray(sourceArrayOfNumbers, startOffset?, pool?) | Creates an instance of this class from an array of numbers, and returns it (create followed by fromArray). See instance.fromArray() for details. If a pool is provided as the last parameter, the instance is created inside it. Returns the new instance. |

Instances created using Monomorph automatically have the following methods and properties (in addition to properties derived from your schema):

| Method | Description | | --- | --- | | instance.copy(otherInstance) | Copy otherInstance into instance, recursively. Returns this. | | instance.set(dataObj) | Set only the fields specified in dataObj on instance. Recursive, but only updates fields that are defined on dataObj. Returns this. | | instance.reset(dataObj?) | Sets the fields specified in dataObj on instance. Recursive. Any fields that are missing from dataObj are reset to the defaults. Returns this. | | instance.destroy() | Puts instance back into its pool for future reuse. Returns this. | | instance.isDestroyed() | Returns true if instance is currently destroyed, false otherwise. Note that if that instance is reused, this method would return false (i.e. not destroyed). Returns boolean. | | instance.toArray(targetArrayOfNumbers, startOffset?) | Encodes this instance (recursively, including all nested types) into numbers. targetArrayOfNumbers is expected to be a Float64Array, Float32Array or a JS array (created with [] syntax or new Array()). startOffset is where in the target array this instance should start (0 by default). Returns a number indicating the next index to write to (i.e. you can use it as startOffset to a future toArray() call). | | instance.fromArray(sourceArrayOfNumbers, startOffset?) | Populates this instance (recursively, including all nested types) from the data in an array of numbers. sourceArrayOfNumbers is expected to be a Float64Array, Float32Array or a JS array (created with [] syntax or new Array()), that was populated using toArray(). startOffset is where in the source array this instance's data starts (0 by default). Returns a number indicating the next index to read from (i.e. you can use it as startOffset to a future fromArray() call). | | instance.fromArrayNoReferences(sourceArrayOfNumbers, startOffset?) | Used for handling circular references, this is like .fromArray(), but skips setting reference fields (ReferencyType, LazyReferenceType). See tests/serialization.test.ts for in-depth examples. | | instance.fromArrayOnlyReferences(sourceArrayOfNumbers, startOffset?) | Used for handling circular references, this is like .fromArray(), but skips setting fields that are NOT references. See tests/serialization.test.ts for in-depth examples. |

| Property | Description | | --- | --- | | instance.pool | The pool this instance is in, or null if it is not in a pool. | | instance.poolIndex | Usually for internal use. The index of this instance in its pool, or -1 if it is not in a pool. | | instance.poolVersion | Usually for internal use. This is a number that increments whenever this instance is destroyed, as well as when it is reused, so you can determine if a reference is stale (handled automatically by ReferenceType, ReferenceListType, LazyReferenceType and LazyReferenceListType). |

The TheClass.Pool class has the following methods and properties:

| Method | Description | | --- | --- | | constructor(maxLength?) | Pools are created with normal new syntax, e.g. const pool = new TheClass.Pool(). If maxLength is specified, the pool will not grow past that number of instances, otherwise maxLength defaults to Infinity. | | pool.create(dataObj?) | Creates an instance of TheClass inside this pool. This is like calling TheClass.create(dataObj, pool). Returns the new instance. | | pool.toArray(targetArrayOfNumbers, startOffset?) | Like instance.toArray but for an entire pool. A pool created from this array will have the same instances, including empty instances, inside it. | | pool.fromArray(sourceArrayOfNumbers, startOffset?) | Like instance.fromArray but for an entire pool. | | pool.fromArrayNoReferences(sourceArrayOfNumbers, startOffset?) | Like instance.fromArrayNoReferences but for an entire pool. | | pool.fromArrayOnlyReferences(sourceArrayOfNumbers, startOffset?) | Like instance.fromArrayOnlyReferences but for an entire pool. |

| Property | Description | | --- | --- | | pool.length | The current number of instances in this pool that are not destroyed. | | pool.maxLength | The maximum number of instances allowed in this pool. | | pool.array | Usually for internal use. The array that has the instances, including destroyed ones and potential nulls. | | pool.freeIndices | Usually for internal use. An array that has the indices instances of the instances that are currently destroyed and can be reused. |

Additional and advanced examples

There are many examples written in Typescript in the tests folder, including CustomType, ReferenceListType and others, as well as handling the serialization and deserialization of pools and references, including circular references.

Roadmap

  • More official benchmarks; basic benchmarks were added in version 1.0.1 but more extensive benchmarks would be good
  • Fixing bugs: while Monomorph is heavily used in our physics library Bounce, there may be bugs around edge cases and untested complex combinations. Please file an issue if you run into problems
  • More supported types: Strings are partially implemented and not considered fully supported yet, other types will be added based on requests and code contributions
  • Want to see something here? Please create an issue on codeberg

How to contribute

If you like this project and would like to support our work, please consider contributing code via pull requests, or donating via open collective. Contributions are greatly appreciated!