Skip to content

QModel ​

The QModel class is the heart of the library. It's an abstract base class that provides all the serialization, deserialization, and mocking capabilities to your models.

Usage ​

Extend QModel and pass your data interface as a generic type:

typescript
import { QModel } from 'quickmodel';

interface IUser {
	id: number;
	name: string;
}

class User extends QModel<IUser> {
	// Your class properties
}

Instantiation Methods ​

QuickModel offers several ways to create instances, depending on your needs.

The most common and standard way.

typescript
const user = new User({
	id: 1,
	name: 'John',
});

Type Safety (Recommended)

For strict type checking between your interface and your class, it is highly recommended to use IQImplements. This helper ensures your class properties match your interface definition. Learn more.

typescript
class User extends QModel<IUser> implements IQImplements<IUser, IUserTransform> { ... }

2. Factory Method (create) ​

Useful for functional programming patterns or when mapping arrays.

typescript
const user = User.create({
	id: 1,
	name: 'John',
});

// Mapping example
const users = dataArray.map(User.create);

3. From JSON String ​

Automatically parses JSON string and then transforms types.

typescript
const json = '{"id":1,"name":"John","createdAt":"2024-01-01"}';
const user = User.fromJSON(json);

4. Cloning ​

Creates a deep copy of an existing instance.

typescript
const clone = user.$qCopy();

5. Readonly Instance ​

Creates a deeply frozen (immutable) instance. Any attempt to modify it will throw an error in strict mode.

typescript
const readonlyUser = User.createReadonly({
	id: 1,
	name: 'John',
});

// readonlyUser.name = 'Jane'; // Error!

6. Bulk Creation (createMany) ​

Creates multiple instances from an array. All items are processed regardless of individual failures — items that fail isValid() are collected in errors[] and excluded from instances[] by default.

typescript
const rawList = [
	{ name: 'Alice', age: 30 },
	{ name: 'Minor', age: 10 }, // fails @QRule
	{ name: 'Bob', age: 25 },
];

const { instances, errors } = UserModel.createMany(rawList);

console.log(instances.length); // 2  (Alice + Bob)
console.log(errors.length); // 1

console.log(errors[0].index); // 1
console.log(errors[0].instance.name); // 'Minor'
console.log(errors[0].errors); // [{ field: 'age', message: 'Must be adult', value: 10 }]

To include invalid instances in the result too:

typescript
const { instances, errors } = UserModel.createMany(rawList, {
	includeErrorInstances: true,
});
// instances.length === 3 — all three, including Minor
// errors.length   === 1 — error list still populated
OptionTypeDefaultDescription
includeErrorInstancesbooleanfalseAlso put invalid instances in instances[]

Lifecycle ​

When a model is instantiated, the following happens:

  1. Constructor Called: Data is received.
  2. Initialization: this.initialize() is called internally.
  3. Deserialization: Data is processed, transformers are applied (string -> Date).
  4. Hydration: Properties are assigned to the instance.
  5. Validation: Optional validation steps are run.

Core Methods ​

serialize() ​

Converts the model back to a plain JavaScript object, reversing transformations (e.g., Date -> ISO string).

typescript
const plain = user.$qSerialize();

toJSON() ​

Implements the JS toJSON protocol. Returns a plain object (same as $qSerialize()) so that JSON.stringify(model) works correctly. Note: calling user.toJSON() directly also returns a plain object — use user.$qToJSON() if you need a JSON string.

typescript
// JS protocol — works with JSON.stringify:
const jsonStr = JSON.stringify(user);
// '{ "id": 1, "name": "John", ... }'

// Direct call returns a plain object:
const plain = user.toJSON();
// { id: 1, name: 'John', ... }

// Explicit JSON string:
const jsonString = user.$qToJSON();
// '{ "id": 1, "name": "John", ... }'

toInterface() ​

Returns the data in its original raw format (as defined by the interface), preserving original types (e.g., keeping strings as strings). Useful for forms or checking initial state.

typescript
// If User was created with { createdAt: '2024-01-01' }
const rawData = user.$qToInterface();
// rawData.createdAt is '2024-01-01' (string)

static getMetadata() ​

Returns a map of all decorated properties and their configuration. Useful for building dynamic forms or inspection tools.

typescript
const meta = User.getMetadata();
console.log(meta.get('createdAt').type); // 'Date'

static deserialize(data) ​

Low-level method to hydrate a plain object into a model instance. Equivalent to new Model(data).

typescript
const user = User.deserialize(plainObject);

validationReport() ​

Returns a detailed report of all validation failures, including transformer integrity checks and @QRule business rule violations.

typescript
const report = user.$qValidationReport();
if (!report.valid) {
	console.error(report.errors);
}

isValid() ​

Returns true if the model passes all integrity checks and business rules. Combines checkIntegrity() and checkRules() in a single call.

typescript
if (!user.$qIsValid()) {
	console.error('Model is not valid');
}

State Management & Change Tracking ​

QModel includes powerful built-in tools to track changes, compare states, and manage updates.

hasChanges() / isDirty(field?) ​

Without arguments, returns true if any field has changed since instantiation.

With a field name, returns true if that specific field is dirty.

typescript
const user = new User({ name: 'John', age: 30 });
console.log(user.$qIsDirty()); // false
console.log(user.$qIsDirty('name')); // false

user.name = 'Jane';
console.log(user.$qIsDirty()); // true  — something changed
console.log(user.$qIsDirty('name')); // true  — 'name' changed
console.log(user.$qIsDirty('age')); // false — 'age' did NOT change

TIP

hasChanges() and isDirty() (no argument) are equivalent. isDirty(field) is the new per-field variant.

getChanges() ​

Returns a partial object containing only the fields that have changed. Perfect for generating PATCH payloads.

typescript
const user = new User({ id: 1, name: 'John', age: 30 });

user.age = 31;

const changes = user.$qGetChanges();
// Result: { age: 31 }

$qGetChangedFields() ​

Returns an array of the names of modified properties.

typescript
const fields = user.$qGetChangedFields();
// Result: ['age']

$qReset() ​

Reverts the model instance back to its initial state (the data provided to the constructor).

typescript
user.name = 'Modified';
user.$qReset();
console.log(user.name); // 'John' (Original value)

patch(data) ​

Applies partial updates to the model. Useful for processing API responses or partial form updates.

typescript
user.$qPatch({ age: 32 });
// Only 'age' is updated, other fields remain unchanged

$qGetInitInterface() ​

Returns the original data used to create the instance, in its original format (preserving strings instead of Dates, etc.).

typescript
// Initial input: { createdAt: '2024-01-01' }
const original = user.$qGetInitInterface();
console.log(original.createdAt); // '2024-01-01' (String)

copy(partial?) ​

Creates a new independent instance. Optionally merges the current state with partial data. The original instance is never modified.

typescript
const user = new User({ id: 1, name: 'John', age: 30 });

// Deep copy without changes
const clone = user.$qCopy();

// Copy with partial overrides
const updated = user.$qCopy({ age: 31 });

console.log(user.age); // 30  — original untouched
console.log(updated.age); // 31  — new instance
console.log(updated.name); // 'John' — preserved

// The new instance has its own change tracking
updated.name = 'Jane';
console.log(updated.$qIsDirty()); // true
console.log(updated.$qIsDirty('age')); // false — 31 is its baseline
console.log(updated.$qIsDirty('name')); // true  — changed after merge

NOTE

copy() returns a fully independent instance with its own change tracking. The copied state becomes the new baseline — isDirty() is false immediately after copy(), and reset() reverts to the copied state (not the original).

Mocking ​

Every QModel has a built-in static mock generator.

typescript
// Generate one instance
const fakeUser = User.mock().random();

// Generate array of 10 instances
const fakeUsers = User.mock().array(10);

// Generate with specific overrides
const admin = User.mock().random({ role: 'admin' });

TIP

For more details on powerful mock generation features, check out the Mocks Guide.

Performance ​

⚡ Performance Benchmark

Hover the bars to see details

10 cycles of 1k objects — throughput validation. class-transformer excluded (not a validator — needs class-validator separately). Plain JS excluded (no validation). arktype is fastest native TS validator. joi and vest are focused on form/business logic rules.

QuickModel
2k ops/s
Plain JS
95k ops/s
›› much faster (see note ↑)

Not applicable in this scenario: Immer

← slower       faster →