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

freezedts

v0.15.1

Published

Immutable class management for TypeScript via code generation

Readme

freezedts

npm version Node.js CI CodeQL

Immutable class generation for TypeScript — deep copying, value equality, and runtime immutability via decorators and code generation.

A TypeScript port of Dart's freezed package by Remi Rousselet.

Table of Contents

Motivation

TypeScript has no native support for immutable classes with named parameters. Achieving truly immutable data classes requires significant boilerplate:

  • readonly on every property
  • Object.freeze() in every constructor
  • Manual copyWith methods for creating modified copies
  • Manual equals methods for structural comparison
  • Recursive freezing of nested collections

freezedts eliminates this boilerplate. You write a class with a decorator, and the generator produces an abstract base class that handles immutability, deep copying, value equality, toString(), and collection freezing.

// You write this:
@freezed()
class Person extends $Person {
  constructor(params: { firstName: string; lastName: string; age: number }) {
    super(params);
  }
}

The generator produces $Person with:

  • readonly properties
  • Object.freeze(this) in the constructor
  • person.with({ age: 31 }) for copying
  • person.equals(other) for deep structural comparison
  • person.toString()"Person(firstName: John, lastName: Smith, age: 30)"
  • Recursive freezing of arrays, Maps, and Sets

Installation

npm install freezedts
npm install -D freezedts-cli

freezedts is the runtime — add it as a production dependency. It has zero transitive dependencies.

freezedts-cli is the code generator — add it as a dev dependency. It depends on ts-morph for AST parsing.

Requirements: TypeScript 6, ESM, TC39 stage 3 decorators.

Running the Generator

# Generate .freezed.ts files for all source files in the current directory
npx freezedts

# Generate for a specific directory
npx freezedts src

# Watch mode — regenerate on file changes
npx freezedts --watch
npx freezedts -w src

# Use a custom config file
npx freezedts --config path/to/freezedts.config.json
npx freezedts -c custom.json -w src

# Force generating all freezed files
npx freezedts --force

# Show help
npx freezedts --help
npx freezedts -h

The generator scans for .ts files containing @freezed() classes and produces .freezed.ts files alongside them.

Only changed files are regenerated (mtime-based).

Creating Classes

Basic Usage

  1. Create your source file (e.g., person.ts):
import { freezed } from 'freezedts';
import { $Person } from './person.freezed.ts';

@freezed()
class Person extends $Person {
  constructor(params: { firstName: string; lastName: string; age: number }) {
    super(params);
  }
}
  1. Run the generator:
npx freezedts
  1. The generated person.freezed.ts provides an abstract base class $Person with readonly properties, Object.freeze(this), with(), equals(), and toString().

Where a source file contains multiple @freezed classes, they will be generated to the same file.

import { freezed } from 'freezedts';
import { $Person, $Address } from './person.freezed.ts';

@freezed()
class Person extends $Person {
  ...
}

@freezed()
class Address extends $Address {
  ...
}

Field Configuration

Configure defaults and validation in the @freezed() decorator's fields option:

@freezed({
  fields: {
    port: {
      default: 3000,
      assert: (v: number) => v > 0 && v < 65536,
      message: 'port out of range',
    },
    host: { default: 'localhost' },
  },
})
class ServerConfig extends $ServerConfig {
  constructor(params: { name: string; host?: string; port?: number }) {
    super(params);
  }
}

const config = new ServerConfig({ name: 'api' });
config.host; // 'localhost'
config.port; // 3000

new ServerConfig({ name: 'api', port: -1 }); // throws: "port out of range"

Field config options:

| Option | Type | Description | |--------|------|-------------| | default | unknown | Default value when the parameter is undefined | | assert | (value) => boolean | Validation function run at construction time | | message | string | undefined | An optional error message when the assertion fails. If omitted, a basic error message will be generated. |

Collections

Arrays, Maps, and Sets are recursively frozen at construction time. Mutation attempts throw at runtime:

@freezed()
class Team extends $Team {
  constructor(params: { name: string; members: string[]; scores: number[] }) {
    super(params);
  }
}

const team = new Team({ name: 'Alpha', members: ['Alice', 'Bob'], scores: [10, 20] });
team.members.push('Charlie'); // throws TypeError
team.members[0] = 'Zara';    // throws TypeError

Deep Copy

The with() method creates a new frozen instance with selective property overrides:

const alice = new Person({ firstName: 'Alice', lastName: 'Smith', age: 30 });
const bob = alice.with({ firstName: 'Bob' });
// bob → Person(firstName: Bob, lastName: Smith, age: 30)
// alice is unchanged

For nested @freezed types, with supports proxy-chained deep copies:

@freezed()
class Assistant extends $Assistant {
  constructor(params: { name: string }) { super(params); }
}

@freezed()
class Director extends $Director {
  constructor(params: { name: string; assistant: Assistant }) { super(params); }
}

@freezed()
class Company extends $Company {
  constructor(params: { name: string; director: Director }) { super(params); }
}

const co = new Company({
  name: 'Acme',
  director: new Director({
    name: 'Jane',
    assistant: new Assistant({ name: 'John' }),
  }),
});

// Shallow copy
co.with({ name: 'NewCo' });

// Deep copy — update a nested freezed property
co.with.director({ name: 'Larry' });
co.with.director.assistant({ name: 'Sue' });

Deep Equality

The equals() method performs structural comparison:

const a = new Person({ firstName: 'Alice', lastName: 'Smith', age: 30 });
const b = new Person({ firstName: 'Alice', lastName: 'Smith', age: 30 });

a === b;       // false (different instances)
a.equals(b);   // true (same structure)

Configure equality mode per class:

@freezed({ equality: 'deep' })    // default — recursive structural comparison
class DeepPerson extends $DeepPerson { ... }

@freezed({ equality: 'shallow' }) // === for primitives, .equals() for nested freezed types
class ShallowPerson extends $ShallowPerson { ... }

Configuration

Per-Class Configuration

Disable generation of specific methods via the @freezed() decorator:

@freezed({ copyWith: false })   // skip with() generation
@freezed({ equal: false })      // skip equals() generation
@freezed({ toString: false })   // skip toString() generation
@freezed({ copyWith: false, equal: false, toString: false })  // only immutability

Project-Wide Configuration

Create a freezedts.config.json in your project root:

{
  "freezed": {
    "options": {
      "format": true,
      "copyWith": false,
      "equal": false,
      "toString": false
    }
  }
}

All options default to true (enabled) except format which defaults to false.

Resolution Order

Per-class @freezed() options override project-wide freezedts.config.json defaults. If neither specifies a value, the built-in default applies (all features enabled).

per-class @freezed()  →  freezedts.config.json  →  built-in defaults
    (highest priority)                              (lowest priority)

Building the Library

# Install dependencies (sets up workspace symlinks)
npm install

# Build both packages
npm run build

# Run tests
npm test

# Run the generator on this project's source files
npm run generate

# Watch mode for tests
npm run test:watch

The project is structured as an npm workspace with two packages:

  • freezedts — runtime library (zero dependencies)
  • freezedts-cli — code generator (depends on ts-morph)

Other tools:

  • TypeScript 6 with ES2022 target and Node16 module resolution
  • bun:test for testing