Batch Creation & Immutability
Learn how to process arrays of API data with createMany() and create immutable instances with createReadonly().
createMany() — Process Arrays
When you receive an array from the API, createMany() processes all items, separates the valid from the invalid, and gives you full visibility into what failed.
Basic Usage
typescript
import { QModel, Quick, QRule } from 'quickmodel';
interface IEmployee {
id: string;
name: string;
email: string;
salary: number;
department: string;
}
@Quick()
class Employee extends QModel<IEmployee> {
declare id: string;
@QRule({
predicate: (val: string) => val.trim().length >= 2,
message: 'Name must be at least 2 characters',
})
declare name: string;
@QRule({
predicate: (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
message: 'Email format is invalid',
})
declare email: string;
@QRule({
predicate: (val: number) => val >= 0,
message: 'Salary cannot be negative',
})
declare salary: number;
declare department: string;
}
// Bulk import data (some invalid)
const importData: IEmployee[] = [
{
id: '1',
name: 'Alice García',
email: 'alice@company.com',
salary: 65000,
department: 'Eng',
},
{ id: '2', name: 'B', email: 'bad-email', salary: 70000, department: 'HR' }, // ❌
{
id: '3',
name: 'Carlos Ruiz',
email: 'carlos@company.com',
salary: -500,
department: 'Fin',
}, // ❌
{
id: '4',
name: 'Diana Torres',
email: 'diana@company.com',
salary: 72000,
department: 'Mkt',
},
{
id: '5',
name: 'Eve Martínez',
email: 'eve@company.com',
salary: 60000,
department: 'Eng',
},
];
const { instances, errors } = Employee.createMany(importData);
console.log(`Successfully imported: ${instances.length}`); // 3
console.log(`With errors: ${errors.length}`); // 2
// Log the errors
errors.forEach(({ index, instance, errors: errs }) => {
console.log(`Row ${index} (ID: ${instance.id}):`);
errs.forEach(({ field, message }) =>
console.log(` - ${field}: ${message}`)
);
});
// Row 1 (ID: 2):
// - name: Name must be at least 2 characters
// - email: Email format is invalid
// Row 2 (ID: 3):
// - salary: Salary cannot be negative
// Valid instances are real model instances
instances.forEach((emp) => {
console.log(`${emp.name} (${emp.department})`);
});Include Invalid Instances
When you also need to access invalid instances:
typescript
const { instances: all, errors } = Employee.createMany(importData, {
includeErrorInstances: true,
});
console.log(`Total processed: ${all.length}`); // 5 (valid + invalid)
all.forEach((emp) => {
const hasError = errors.some(({ instance }) => instance.id === emp.id);
console.log(`${emp.name}: ${hasError ? '❌ Has errors' : '✅ Valid'}`);
});Real World: CSV Import with Error Feedback
typescript
async function importEmployees(csvData: IEmployee[]): Promise<void> {
const { instances, errors } = Employee.createMany(csvData);
// Save valid entries
if (instances.length > 0) {
await fetch('/api/employees/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(instances.map((emp) => emp.$qSerialize())),
});
}
// Report errors to the user
if (errors.length > 0) {
const report = errors.map(({ index, errors: errs }) => ({
row: index + 1,
issues: errs.map((e) => e.message),
}));
console.error('Rows with issues:', report);
}
console.log(
`Import complete: ${instances.length} ok, ${errors.length} errors`
);
}createMany() with Type Transformations
createMany() applies all @Quick transformations just like the constructor:
typescript
interface IEvent {
id: string;
name: string;
startDate: string;
endDate: string;
attendees: string[];
}
@Quick({ startDate: Date, endDate: Date, attendees: Set })
class Event extends QModel<IEvent> {
declare id: string;
declare name: string;
declare startDate: Date;
declare endDate: Date;
declare attendees: Set<string>;
}
const rawEvents: IEvent[] = [
{
id: 'E1',
name: 'TS Conf',
startDate: '2026-06-01',
endDate: '2026-06-03',
attendees: ['alice', 'bob'],
},
{
id: 'E2',
name: 'Hackathon',
startDate: '2026-07-15',
endDate: '2026-07-16',
attendees: ['bob', 'bob'],
},
];
const { instances } = Event.createMany(rawEvents);
console.log(instances[0].startDate instanceof Date); // true ✅
console.log(instances[1].attendees instanceof Set); // true ✅
console.log(instances[1].attendees.size); // 1 (duplicate removed)createReadonly() — Immutable Instances
When you want to guarantee that a model cannot be accidentally mutated (configuration, read data, constants):
Basic Usage
typescript
interface IConfig {
apiUrl: string;
timeout: number;
retries: number;
features: string[];
}
@Quick({ features: Set })
class AppConfig extends QModel<IConfig> {
declare apiUrl: string;
declare timeout: number;
declare retries: number;
declare features: Set<string>;
}
const config = AppConfig.createReadonly({
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
features: ['auth', 'payments', 'analytics'],
});
// ✅ Normal read access
console.log(config.apiUrl); // 'https://api.example.com'
console.log(config.timeout); // 5000
// ❌ Any mutation throws TypeError in strict mode
try {
(config as any).apiUrl = 'http://hacked.com';
} catch {
console.log('Immutable: cannot be modified');
}
console.log(Object.isFrozen(config)); // true (deep freeze)Global Configuration Objects
typescript
// config/app.config.ts
const RAW_CONFIG = {
apiUrl: process.env.API_URL ?? 'https://api.example.com',
timeout: 5000,
retries: 3,
features: ['auth', 'payments'],
};
export const APP_CONFIG = AppConfig.createReadonly(RAW_CONFIG);
// Exports a config object nobody can accidentally mutateDifference from create()
typescript
// ✅ create(): mutable instance
const mutableUser = User.create({ name: 'Alice', email: 'alice@test.com' });
mutableUser.name = 'Beatrice'; // OK
// ✅ createReadonly(): deep immutable instance
const readonlyUser = User.createReadonly({
name: 'Alice',
email: 'alice@test.com',
});
readonlyUser.name = 'Beatrice'; // ❌ TypeError: Cannot assign to read only propertyImmutability in Tests
typescript
// Readonly instances are perfect as immutable test fixtures
const FIXTURE_USER = User.createReadonly({
id: '1',
name: 'Test User',
email: 'test@example.com',
createdAt: '2026-01-01T00:00:00.000Z',
});
// No test can accidentally mutate the fixture
describe('UserCard', () => {
it('renders name correctly', () => {
render(<UserCard user={FIXTURE_USER} />);
expect(screen.getByText(FIXTURE_USER.name)).toBeInTheDocument();
});
});Combining createMany() and createReadonly()
typescript
interface IPermission {
code: string;
description: string;
resource: string;
actions: string[];
}
@Quick({ actions: Set })
class Permission extends QModel<IPermission> {
declare code: string;
declare description: string;
declare resource: string;
declare actions: Set<string>;
}
const rawPermissions: IPermission[] = [
{
code: 'read:users',
description: 'Read users',
resource: 'users',
actions: ['GET'],
},
{
code: 'write:users',
description: 'Write users',
resource: 'users',
actions: ['POST', 'PUT'],
},
{
code: 'delete:users',
description: 'Delete users',
resource: 'users',
actions: ['DELETE'],
},
];
const { instances: permissions } = Permission.createMany(rawPermissions);
// Immutable lookup map
const PERMISSIONS_MAP = Object.freeze(
new Map(permissions.map((perm) => [perm.code, perm]))
);
function hasPermission(code: string, action: string): boolean {
const perm = PERMISSIONS_MAP.get(code);
return perm?.actions.has(action) ?? false;
}
console.log(hasPermission('read:users', 'GET')); // true
console.log(hasPermission('read:users', 'DELETE')); // falseBest Practices
typescript
// ✅ createMany() for API or CSV imports
const { instances, errors } = MyModel.createMany(apiArray);
// ✅ createReadonly() for configuration and constants
export const CONFIG = ConfigModel.createReadonly(rawConfig);
// ✅ Always inspect errors[] when processing external data
if (errors.length > 0) {
logger.warn(`${errors.length} records failed validation`);
}
// ❌ Don't use createReadonly() for data you need to mutate
const user = User.createReadonly(data); // ❌ if you'll do user.name = '...' later
const user = User.create(data); // ✅ use create() for mutable dataNext Steps
- Validation - Business rules with
@QRule - Mocks & Testing - Mock models for tests