Strict Runtime Type Checker for JavaScript Objects
TypeØmatica uses JavaScript Proxies to enforce type safety at runtime, exactly as TypeScript expects at compile time. Once a property is assigned a value, its type is locked and cannot be changed.
import { BaseClass } from 'typeomatica';
class User extends BaseClass {
declare name: string;
declare age: number;
constructor() {
super();
this.name = 'default';
this.age = 0;
}
}
const user = new User();
// ✓ Valid assignments
user.name = 'John';
user.age = 25;
// ✗ Runtime error (even if TypeScript is bypassed with @ts-ignore)
user.age = '25'; // TypeError: Type MismatchImportant: initialized class fields (
name = 'default') are stored as own properties and bypass the proxy. Usedeclare name: string;plus constructor assignment, or seeFieldConstructorfor custom descriptors — or callfinalize(instance)to re-establish hiddenly-added fields as guarded ones (see below).
TypeØmatica records every field that passes through its define machinery,
per instance. Fields added with define semantics (class field initializers,
Object.defineProperty) bypass the proxy — finalize re-establishes them
as guarded fields after the fact:
getConstructedFields(instance)—Setof field names that certainly passed through the machinery (a copy; mutating it affects nothing).finalize(instance)— auto mode: every hiddenly-added own field is deleted and re-established through the define machinery; sets the finalized flag.finalizeBy(instance, fields)— partial mode: same for a listed subset; does not set the finalized flag.isFinalized(instance)—trueonly if autofinalizeran; manual or partial finalization leaves itfalse.unwrap(instance, field)— turns a guarded field back into a plain value property: boxed primitives are read back via.valueOf(), objects and nullish values are placed as-is. Allowed only for fields re-established byfinalize/finalizeBy— those stayconfigurable: trueby design. Fields guarded since construction are non-configurable forever; that lock is the essential design of the library, not an accident.
Designed as the postCreation-hook companion for mnemonica chains: at each
construction level, diff the instance's own descriptors against
getConstructedFields(instance) to find what bypassed the proxy.
TypeØmatica ships both CommonJS and ESM builds:
- CommonJS:
require('typeomatica')resolves tolib/index.js. - ESM:
import { BaseClass, FieldConstructor } from 'typeomatica'resolves tolib/esm/esm.js. - The dual build is produced by
tsconfig.json(CJS) andtsconfig.esm.json(ESM). package.jsonexportsmaps".".requiretolib/index.jsand".".importtolib/esm/esm.js.
npm run build # clean build of lib/ and lib/esm/
npm run test:cov # Jest CJS tests + 100% coverage
npm run test:esm # Vitest true-ESM import tests
npm run examples # run all examples/
npm run lint:src # ESLint on src/src/index.ts— shared core:BaseClass,BaseConstructorPrototype(default export),@Strictdecorator, proxy handlers, and CJSmodule.exportssetup.src/esm.ts— ESM entry point that re-exports the default and named bindings fromsrc/index.ts.src/fields.ts—FieldConstructorclass for custom property descriptors.src/types/*.ts— type-category handlers (primitives, objects, functions, special, nullish).test/index.ts— Jest CJS test suite (70 tests, 100% coverage).test/esm/— Vitest tests that exercise the actual ESMexportsmap, including the native class-fields (define semantics) suite.examples/— runnable integration examples.
- Jest CJS tests must keep 100% coverage across statements, branches, functions, and lines.
- Code paths that can only be reached through true ESM imports are covered by Vitest in
test/esm/. - Istanbul ignore hints are preserved because
tsconfig.jsonsetsremoveComments: false.
- What is TypeØmatica?
- Installation
- Core Concepts
- Module Support
- Development
- API Reference
- Usage Patterns
- Type Examples
- Working with Wrapped Values
- Error Reference
- Integration with Mnemonica
TypeØmatica provides strict runtime type checking for JavaScript objects using Proxy-based interception. It ensures that once a property is initialized with a type, that type cannot be violated at runtime.
Key Features:
- Runtime type enforcement (complements TypeScript's compile-time checks)
- Proxy-based property interception
- Type locking after initial assignment
- Prevents prototype mutation, property redefinition, and deletion
npm install typeomaticaTypeØmatica wraps objects with JavaScript Proxies that intercept:
get- Property reads (undefined properties returnundefined)set- Property writes (with type validation)setPrototypeOf- Blocks prototype changesdefineProperty- Blocks property redefinitiondeleteProperty- Blocks property deletion
| Category | Types | Behavior |
|---|---|---|
primitives |
string, number, boolean | Type-locked accessors |
special |
bigint, symbol, undefined | Same typeof required |
nullish |
null | Any assignment throws after initialization |
objects |
object, arrays, dates | Same constructor type required |
functions |
methods | Restricted on data types |
- For AI agents / contributors: see
AGENTS.mdfor the file map, build pipeline, coverage rules, and conventions. - For human readers: see
FOR_HUMANS.mdfor the motivation, mental model, and friendly examples. - Runnable patterns: see
EXAMPLES.mdfor a catalog of all examples.
TypeØmatica exposes three APIs that all do the same underlying thing — attach a Proxy to a prototype chain so property writes are type-checked and the class prototype is frozen. They differ in how you create and attach that prototype.
BaseClass |
BasePrototype / BaseConstructorPrototype |
@Strict() |
|
|---|---|---|---|
| How you use it | class X extends BaseClass { ... } |
class X extends BasePrototype({...}) { ... } or const X = BasePrototype({...}) |
@Strict({...}) class X { ... } |
| Wiring moment | First time an instance is constructed | First time an instance is constructed | When the class is decorated (module load) |
| Initial state | Can pass a new target per instance via super({...}) |
Target is bound at class/factory creation; super(...) args are ignored because the constructor is bound |
Target/options are bound at decoration |
| Prototype chain shape | X.prototype → … → classDirectlyExtendingBase.prototype → proxy → target |
X.prototype → proxy → target (class usage) |
X.prototype → plain replacer → proxy → target |
| Function-constructor support | No | Yes — BasePrototype can return a constructor function |
No |
| Default export? | No, it is a named export/property | Yes, require('typeomatica') is BaseConstructorPrototype |
No, exported as Strict |
BaseConstructorPrototype is the lowest-level API. It returns an instance that is designed to be assigned as the prototype of an existing constructor function using Object.setPrototypeOf.
import BasePrototype from 'typeomatica';
const MyConstructor = function (this: { value: number }) {
this.value = 0;
};
const baseProto = new BasePrototype({ value: 0 });
Object.setPrototypeOf(MyConstructor.prototype, baseProto);
const instance = new MyConstructor();
instance.value = 42; // ✓ Works
// @ts-ignore
instance.value = '42'; // ✗ TypeError: Type MismatchUse this when you already have a constructor function and need to inject type checking into its prototype chain manually, or when you want a factory that returns a constructor function.
BaseClass is the same mechanism wrapped in a class. When you extends BaseClass, TypeØmatica internally sets the proxy prototype for you.
import { BaseClass } from 'typeomatica';
class MyClass extends BaseClass {
declare value: number;
constructor() {
super({ value: 0 });
this.value = this.value;
}
}super() accepts an optional initial-state object and TypeomaticaOptions. Because the proxy target is set up during super(), you usually re-assign the inherited values inside the constructor (as shown above) so they pass through the proxy and become type-locked. Unlike BasePrototype, BaseClass lets you pass a new initial-state object per instance.
@Strict() does the same prototype injection but as a class decorator. It accepts the same optional arguments as BaseConstructorPrototype:
- First argument: initial-state object (
{ starterProp: value }). - Second argument:
TypeomaticaOptions(e.g.,{ strictAccessCheck: true }).
import { Strict } from 'typeomatica';
@Strict({ value: 0 }, { strictAccessCheck: true })
class MyClass {
declare value: number;
constructor() {
this.value = 0;
}
}Use @Strict() when you want a clean class decorator and need to pass initial state or options without changing the class extends clause.
- Use
BaseClassfor modernextends-based classes, especially when you want per-instance initial state throughsuper({...}). - Use
BasePrototype(...)when you need a factory that returns a constructor, or when working with function constructors. - Use
@Strict()when you want to Typeømatica-wrap an existing class without changing itsextendsclause.
The primary class for strict-type objects. super() accepts an optional initial-state object and TypeomaticaOptions.
import { BaseClass } from 'typeomatica';
class MyClass extends BaseClass {
declare field: string;
constructor() {
super({ field: 'value' });
this.field = this.field;
}
}Functional equivalent of BaseClass. The default export of the package is BaseConstructorPrototype.
Form 1 — call without new to get a base constructor:
import BasePrototype from 'typeomatica';
// or: import { BaseConstructorPrototype } from 'typeomatica';
const Base = BasePrototype({ initialProp: 123 });
class MyClass extends Base { }Form 2 — call with new to get a prototype object for Object.setPrototypeOf:
import BasePrototype from 'typeomatica';
const MyConstructor = function (this: { initialProp: number }) {
this.initialProp = 0;
};
const baseProto = new BasePrototype({ initialProp: 123 });
Object.setPrototypeOf(MyConstructor.prototype, baseProto);
const instance = new MyConstructor();Apply strict typing without extending BaseClass.
import { Strict } from 'typeomatica';
@Strict()
class MyClass {
declare field: number;
constructor() {
this.field = 0;
}
}Decorator Arguments:
- First argument: optional target object used as the prototype base
- Second argument: optional options object
Build custom property descriptors with controlled getters and setters. When a FieldConstructor instance is assigned to a BaseClass/BasePrototype property, its get and set methods are used directly as the property descriptor.
import { BaseClass, FieldConstructor, SymbolInitialValue } from 'typeomatica';
// Default FieldConstructor: read-only field
const createdAt = new FieldConstructor(new Date());
class Record extends BaseClass {
createdAt = createdAt as unknown | Date;
}
const record = new Record();
console.log(record.createdAt); // ✓ Works
// @ts-ignore
record.createdAt = new Date(); // ✗ TypeError: Re-Assirnment is ForbiddenCustom read/write field:
class MutableField extends FieldConstructor {
_value: string;
constructor(value: string) {
super(value);
this._value = value;
Reflect.defineProperty(this, 'enumerable', { value: true });
const self = this;
Reflect.defineProperty(this, 'get', {
get() {
return function () { return self._value; };
},
enumerable: true
});
Reflect.defineProperty(this, 'set', {
get() {
return function (value: string) { self._value = value; };
},
enumerable: true
});
}
}
const displayName = new MutableField('Guest');
class User extends BaseClass {
displayName = displayName as unknown | string;
}
const user = new User();
console.log(user.displayName); // 'Guest'
user.displayName = 'Alice'; // ✓ Works
console.log(user.displayName); // 'Alice'Function-constructor getter:
If you prefer function constructors over classes, subclass FieldConstructor through the prototype chain:
function TimestampField(this: any, value: Date) {
return Reflect.construct(FieldConstructor, [value], TimestampField);
}
TimestampField.prototype = Object.create(FieldConstructor.prototype);
Reflect.defineProperty(TimestampField.prototype, 'constructor', {
value: TimestampField,
writable: true,
configurable: true
});
Reflect.defineProperty(TimestampField.prototype, 'get', {
get() {
const self = this as FieldConstructor;
return function () {
return self[SymbolInitialValue];
};
},
enumerable: true
});
const created = new (TimestampField as any)(new Date());
class Event extends BaseClass {
created = created as unknown | Date;
}
const event = new Event();
console.log(event.created); // Date instanceBuffer field with toJSON support:
Wrap a Buffer so it serializes cleanly with JSON.stringify:
class BufferField extends FieldConstructor {
buffer: Buffer;
constructor(value: Buffer) {
super(value);
this.buffer = value;
Reflect.defineProperty(this, 'enumerable', { value: true });
const self = this;
Reflect.defineProperty(this, 'get', {
get() {
return function () {
return Object.create(self.buffer, {
toJSON: {
value: () => self.buffer.toJSON()
},
valueOf: {
value: () => self.buffer
}
});
};
},
enumerable: true
});
}
}
const payload = new BufferField(Buffer.from('hello'));
class Message extends BaseClass {
payload = payload as unknown | Buffer;
}
const message = new Message();
console.log(JSON.stringify(message.payload)); // {"type":"Buffer","data":[104,101,108,108,111]}Typed unsigned integer fields:
Create UInt8, UInt16, and UInt32 fields that clamp values, validate types, and expose Symbol.toPrimitive for arithmetic:
function createUIntField(bits: 8 | 16 | 32) {
const max = 2 ** bits - 1;
return class UIntField extends FieldConstructor {
numericValue: number;
constructor(value: number) {
super(value);
const initial = parseInt(String(value), 10);
if (isNaN(initial)) {
throw new TypeError('Type Mismatch');
}
this.numericValue = Math.max(0, Math.min(max, initial));
Reflect.defineProperty(this, 'enumerable', { value: true });
const self = this;
Reflect.defineProperty(this, 'get', {
get() {
return function () {
return {
valueOf: () => self.numericValue,
[Symbol.toPrimitive]: (hint: string) => {
return hint === 'number' ? self.numericValue : String(self.numericValue);
}
};
};
},
enumerable: true
});
Reflect.defineProperty(this, 'set', {
get() {
return function (newValue: number) {
const num = parseInt(String(newValue), 10);
if (isNaN(num)) {
throw new TypeError('Type Mismatch');
}
self.numericValue = Math.max(0, Math.min(max, num));
};
},
enumerable: true
});
}
};
}
const UInt8Field = createUIntField(8);
const UInt16Field = createUIntField(16);
const UInt32Field = createUIntField(32);
class Registers extends BaseClass {
flags = new UInt8Field(0) as unknown | number;
count = new UInt16Field(1000) as unknown | number;
timestamp = new UInt32Field(Date.now()) as unknown | number;
}
const registers = new Registers();
registers.flags = 255; // ✓ Works
// @ts-ignore
registers.flags = 256; // ✗ clamps to 255 (or throw if you prefer)
// @ts-ignore
registers.flags = 'not a number'; // ✗ TypeError: Type Mismatch
const doubled = registers.count.valueOf() * 2;Notes:
- Set
enumerable: trueon the instance if the property should appear inObject.keys(). - Reusing the same
FieldConstructorinstance across multiple class instances will share state. - Access the original constructor value through
FieldConstructor.SymbolInitialValueorinstance[SymbolInitialValue].
interface TypeomaticaOptions {
strictAccessCheck?: boolean; // default: false
frozenPrototypes?: boolean; // default: true
}Options:
strictAccessCheck: true- Enables strict receiver checking (throwsACCESS_DENIEDerror when property is accessed from wrong context)frozenPrototypes: false- Keeps class prototypes mutable so methods/properties can be added at runtime (foot-gun allowed)
// With BaseClass - strict access checking enabled
class SecureData extends BaseClass {
declare secret: string;
constructor() {
super(undefined, { strictAccessCheck: true });
this.secret = '';
}
}
// With BasePrototype - strict access checking enabled
const SecureBase = BasePrototype({ data: '' }, { strictAccessCheck: true });
class User extends SecureBase {
declare name: string;
constructor() {
super();
this.name = '';
}
}
// With @Strict decorator - strict access checking enabled
@Strict({ starterProp: true }, { strictAccessCheck: true })
class Product {
declare price: number;
constructor() {
this.price = 0;
}
}
// With BaseClass - allow runtime prototype mutation (not recommended)
class ExtensibleBase extends BaseClass {
declare value: number;
constructor() {
super(undefined, { frozenPrototypes: false });
this.value = 0;
}
}
ExtensibleBase.prototype.helper = () => 'ok';import BasePrototype, {
BaseConstructorPrototype, // Functional equivalent of BaseClass
BaseClass, // Primary strict-type base class
Strict, // Decorator for strict typing
SymbolTypeomaticaProxyReference, // Access proxy internals
SymbolInitialValue, // Access original values
baseTarget // Utility to create a null-prototype object
} from 'typeomatica';
// FieldConstructor is also available as a named export
import { FieldConstructor } from 'typeomatica';import { BaseClass } from 'typeomatica';
class User extends BaseClass {
declare name: string;
declare age: number;
declare active: boolean;
constructor() {
super();
this.name = '';
this.age = 0;
this.active = true;
}
}
const user = new User();
user.name = 'John'; // ✓ Works
user.age = 25; // ✓ Works
// @ts-ignore
user.age = '25'; // ✗ TypeError: Type Mismatchimport { Strict } from 'typeomatica';
@Strict()
class Product {
declare id: number;
declare title: string;
declare price: number;
constructor() {
this.id = 0;
this.title = '';
this.price = 0;
}
}
const product = new Product();
product.price = 29.99; // ✓ Works
// @ts-ignore
product.price = '$29.99'; // ✗ TypeError: Type Mismatchimport { BaseClass } from 'typeomatica';
class UserData {
declare name: string;
declare age: number;
constructor() {
this.name = 'default';
this.age = 0;
}
}
// Inject type checking into prototype chain
Object.setPrototypeOf(UserData.prototype, new BaseClass());
const user = new UserData();
user.name = 'John'; // ✓ Works
// @ts-ignore
user.name = 123; // ✗ TypeError: Type Mismatchimport { BaseClass } from 'typeomatica';
class Primitives extends BaseClass {
declare str: string;
declare num: number;
declare bool: boolean;
declare bigint: bigint;
constructor() {
super();
this.str = 'hello';
this.num = 42;
this.bool = true;
this.bigint = BigInt(100);
}
}
const p = new Primitives();
p.str = 'world'; // ✓ Works
p.num = 100; // ✓ Works
// @ts-ignore
p.str = 123; // ✗ TypeError: Type Mismatchclass Nullable extends BaseClass {
declare nullValue: null;
declare undefinedValue: undefined;
constructor() {
super();
this.nullValue = null;
this.undefinedValue = undefined;
}
}
const n = new Nullable();
n.nullValue = null; // ✓ First assignment creates the property
// @ts-ignore
n.nullValue = null; // ✗ TypeError: Type Mismatch (null cannot be reassigned)
// @ts-ignore
n.nullValue = undefined; // ✗ TypeError: Type Mismatch
n.undefinedValue = undefined; // ✓ Works
// @ts-ignore
n.undefinedValue = 123; // ✗ TypeError: Type Mismatchclass WithObject extends BaseClass {
declare data: object;
declare list: number[];
constructor() {
super();
this.data = {};
this.list = [];
}
}
const w = new WithObject();
w.data = { a: 1 }; // ✓ Works
w.list = [1, 2, 3]; // ✓ Works
// @ts-ignore
w.data = 123; // ✗ TypeError: Type Mismatch
// @ts-ignore
w.data = new Set(); // ✗ TypeError: Type Mismatch (Set !== Object)TypeØmatica wraps primitives to enforce type safety. Use valueOf() for operations.
class Calc extends BaseClass {
declare count: number;
constructor() {
super();
this.count = 10;
}
}
const calc = new Calc();
// ✗ Direct arithmetic throws
const result = calc.count + 5; // ReferenceError: Value Access Denied
// ✓ Use valueOf()
const result = calc.count.valueOf() + 5; // 15
const sum = 3 + +calc.count; // 13class Text extends BaseClass {
declare message: string;
constructor() {
super();
this.message = 'hello';
}
}
const text = new Text();
text.message.valueOf().toUpperCase(); // 'HELLO'
text.message.valueOf().length; // 5| Error Message | Error Type | When Thrown |
|---|---|---|
Type Mismatch |
TypeError | Wrong type assigned to property |
Value Access Denied |
ReferenceError | Property accessed from wrong context (only when strictAccessCheck: true) |
Functions are Restricted |
TypeError | Function assigned to data property |
Re-Assirnment is Forbidden |
TypeError | Modifying read-only custom field |
Setting prototype is not allowed! |
Error | Calling Object.setPrototypeOf() |
Defining new Properties is not allowed! |
Error | Calling Object.defineProperty() |
Properties Deletion is not allowed! |
Error | Calling delete on property |
TypeØmatica integrates seamlessly with mnemonica for combined prototype inheritance and runtime type safety.
import { decorate } from 'mnemonica';
import { BaseClass, Strict } from 'typeomatica';
@decorate()
@Strict()
class Entity extends BaseClass {
declare id: string;
declare createdAt: Date;
constructor() {
super();
this.id = '';
this.createdAt = new Date();
}
}
// Works with mnemonica's inheritance system
const User = Entity.define('User', function(this: { email: string }) {
this.email = '';
});
const user = new User();
// @ts-ignore
user.email = 123; // ✗ TypeError: Type MismatchDecorator Order Matters:
@Strict()must come AFTER@decorate()(inner decorator)- Decorators apply bottom-to-top
For complete integration documentation, see mnemonica's TypeØmatica guide.
MIT
Copyright (c) 2019-2024 https://github.com/wentout