Mapper
Schema-to-schema object mapping with compile-time completeness checking, auto-mapping, and zero decorators.
Installation
npm install @cleverbrush/mapper @cleverbrush/schemaWhy use a mapper?
In non-trivial apps, the shape of data differs between layers — API DTOs, domain models, database rows, view models. Manual mapping code is repetitive and silently breaks when you add a new field.
@cleverbrush/mapper uses your existing @cleverbrush/schema definitions to generate mapping functions — and gives you a compile-time error if any target property is unmapped.
Basic usage
import { object, string, number } from '@cleverbrush/schema';
import { mapper } from '@cleverbrush/mapper';
const ApiUser = object({
first_name: string(),
last_name: string(),
birth_year: number()
});
const DomainUser = object({
fullName: string(),
age: number()
});
const registry = mapper().configure(ApiUser, DomainUser, (m) =>
m
.for((t) => t.fullName)
.compute((src) => src.first_name + ' ' + src.last_name)
.for((t) => t.age)
.compute((src) => new Date().getFullYear() - src.birth_year)
);
const mapFn = registry.getMapper(ApiUser, DomainUser);
const result = await mapFn({
first_name: 'Jane',
last_name: 'Doe',
birth_year: 1995
});
// { fullName: 'Jane Doe', age: 30 }Mapping strategies
| Strategy | Syntax | Purpose |
|---|---|---|
.from() | .for(t => t.x).from(s => s.y) | Copy from a source property |
.compute() | .for(t => t.x).compute(s => ...) | Compute from a sync or async function |
.ignore() | .for(t => t.x).ignore() | Exclude property from output |
| Auto-mapped | (no config needed) | Same-name compatible primitives, or registered nested schemas |
Auto-mapping
Properties with the same name and compatible types are mapped automatically — you only need to configure the differences:
const Source = object({
id: string(),
name: string(),
email: string(),
age: number()
});
const Target = object({
id: string(),
name: string(),
email: string(),
ageGroup: string()
});
const registry = mapper().configure(Source, Target, (m) =>
m
.for((t) => t.ageGroup)
.compute((src) => (src.age < 18 ? 'minor' : 'adult'))
// id, name, email are auto-mapped — no configuration needed
);Nested & array schemas
Register the child mapping first, and the parent mapper automatically uses it for nested objects and array elements:
const Address = object({ city: string(), houseNr: number() });
const AddressDto = object({ city: string() });
const Person = object({ name: string(), address: Address });
const PersonDto = object({ name: string(), address: AddressDto });
const registry = mapper()
.configure(Address, AddressDto, (m) =>
m.for((t) => t.city).from((f) => f.city)
)
.configure(Person, PersonDto, (m) =>
m.for((t) => t.name).from((f) => f.name)
);
// address is auto-mapped using the registered Address→AddressDto mapperSynchronous mapping and query row schemas
Keep the existing configure() API. When the final computations and nested mappings are synchronous, getSyncMapper() returns ordinary values while retaining completeness checks. getMapper() remains asynchronous.
const read = db.users
.select(u => ({ id: u.id, name: u.name }));
const Source = read.rowSchema; // no SQL
const toDto = mapper().configure(Source, UserDto, m => m)
.getSyncMapper(Source, UserDto);
const result = (await read).map(toDto);Known async mappings are rejected; disguised promises or thenables throw at invocation. Configuration never probes callbacks. Fetching, enrichment, authorization and union dispatch remain explicit application code.
Read the multi-file consumer guide and compatibility rulesCompile-time completeness
If you add a new property to the target schema and forget to map it, TypeScript reports an error at build time — not at runtime:
// Target gains a new 'role' property
const Target = object({ name: string(), role: string() });
// ❌ TypeScript error:
// 'role' is unmapped — SYMBOL_UNMAPPED prevents compilation
const registry = mapper().configure(Source, Target, (m) =>
m.for((t) => t.name).from((s) => s.name)
);The registry is immutable — configure() returns a new registry, safe to share and extend.