quickmodel / Quick
Function: Quick()
Quick<
TExtendedTypes,TTypeMap,TAliases>(typeMap?,advancedOptions?,extraOptions?):ClassDecorator
Class decorator that automatically applies @QType() to all properties.
Properties are registered from the data passed to the constructor.
Property Modifiers (! vs declare):
- ✅
!(Definite Assignment): Safe to use.@Quickwraps the constructor and handles initialization, preventing "undefined" overwrites. - ✅
declare: Also safe to use.
No automatic detection - All special types must be explicitly declared:
- Date, BigInt, RegExp, Map, Set, etc. MUST be specified in type mapping
- Without explicit declaration, values are used as-is with TypeScript metadata only
Type Parameters
TExtendedTypes
TExtendedTypes extends IQAnyRecord = IQAnyRecord
TTypeMap
TTypeMap extends IQOptions = IQOptions
TAliases
TAliases extends Record<string, string> = Record<never, never>
Parameters
typeMap?
IQImplements<TTypeMap, TExtendedTypes>
REQUIRED mapping for Set, Map, custom classes, and transformers
advancedOptions?
IQAdvancedOptions<TTypeMap> & object
extraOptions?
IQAdvancedOptions<Record<string, unknown>>
Returns
ClassDecorator
A class decorator function
Examples
✅ Type mapping with constructors:
@Quick({
createdAt: Date,
balance: BigInt,
tags: Set,
metadata: Map,
posts: Post
})
class User extends QModel<IUser> {
declare id: number;
declare createdAt: Date; // Explicitly mapped
declare balance: bigint; // Explicitly mapped
declare tags: Set<string>; // Explicitly mapped
declare metadata: Map<string, any>; // Explicitly mapped
}✅ String literals for basic types (autocomplete):
@Quick({
value: 'bigint', // ← IDE autocomplete!
date: 'date', // ← IDE autocomplete!
pattern: 'regexp', // ← IDE autocomplete!
tags: 'set', // ← IDE autocomplete!
flags: 'map' // ← IDE autocomplete!
})
class Account extends QModel<IAccount> {
declare value: bigint;
declare date: Date;
declare pattern: RegExp;
declare tags: Set<string>;
declare flags: Map<string, boolean>;
}✅ Direct functions (maximum flexibility):
@Quick({
// Direct Math methods
price: (v) => Math.round(v * 100) / 100, // Round to 2 decimals
count: Math.floor, // Floor
percentage: (v) => Math.min(100, Math.max(0, v)), // Clamp 0-100
// String transformations
name: (s) => s.trim().toUpperCase(), // Trim and uppercase
slug: (s) => s.toLowerCase().replace(/\s+/g, '-'), // Slugify
// Encoding/Decoding
encoded: (s) => Buffer.from(s).toString('base64'), // Base64 encode
decoded: (s) => Buffer.from(s, 'base64').toString(), // Base64 decode
// JSON
metadata: JSON.parse, // Parse JSON string
// Business logic custom
tax: (price) => price * 0.21, // Calcula IVA 21%
total: (p) => (p * 1.21) * 0.9 // Precio + IVA - 10% desc
})
class Product extends QModel<IProduct> {
declare price: number;
declare count: number;
declare percentage: number;
declare name: string;
declare slug: string;
declare encoded: string;
declare decoded: string;
declare metadata: any;
declare tax: number;
declare discounted: number;
}✅ Array element types:
@Quick({ dates: [Date, undefined, null] })
class User extends QModel<IUser> {
dates?: (Date | undefined | null)[]; // Transforms strings to Date, preserves undefined/null
}✅ Custom transformer functions:
@Quick({
dates: (arr) => arr.map(d => d ? new Date(d) : d),
custom: (value) => ({ ...value, transformed: true })
})
class User extends QModel<IUser> {
dates?: (Date | undefined | null)[];
custom!: any;
}✅ Dot notation for nested properties:
// Option 1: Decorate nested class (recommended for reusable models)
@Quick({ price: BigInt, createdAt: Date })
class Product extends QModel<IProduct> {
price!: bigint;
createdAt!: Date;
}
@Quick({ product: Product, addedAt: Date })
class CartItem extends QModel<ICartItem> {
product!: Product; // Product already has transformations
addedAt!: Date;
}
// Option 2: Use dot notation (useful for third-party or context-specific transforms)
@Quick({
product: Product,
'product.price': BigInt, // Nested transformation
'product.createdAt': Date, // Nested transformation
addedAt: Date
})
class CartItem extends QModel<ICartItem> {
product!: Product; // All transformations in one place
addedAt!: Date;
}✅ Math functions and string literals with parameters:
@Quick({
price: 'round.2', // Round to 2 decimals
discount: Math.abs, // Math.abs function directly
total: (v) => Math.round(v * 100) / 100, // Custom precision
encoded: 'base64.encode', // Base64 encoding
slug: 'slugify', // Convert to URL slug
hash: 'sha256' // SHA-256 hash
})
class Product extends QModel<IProduct> {
price!: number;
discount!: number;
total!: number;
encoded!: string;
slug!: string;
hash!: string;
}✅ Works with both declare and ! syntax:
@Quick({ tags: Set })
class User extends QModel<IUser> {
id!: number; // ✅ Works
tags!: Set<string>; // ✅ Works with type mapping
}✅ Union types with discriminators (ADVANCED):
When you have arrays that can contain different model types, use discriminators to tell QuickModel how to distinguish between them at runtime.
// Backend interfaces
interface IContent {
type: 'content'; // Discriminator field
text: string;
}
interface IMetadata {
type: 'metadata'; // Discriminator field
tags: string[];
}
// Model classes
class Content extends QModel<IContent> { ... }
class Metadata extends QModel<IMetadata> { ... }
// Use discriminator to handle union types
@Quick({
items: [Content, Metadata], // Declare ALL possible types
}, {
discriminators: {
// Option 1: String discriminator (field name)
// Matches field value with constructor name (case-insensitive)
items: 'type' // Uses data.type to determine Content vs Metadata
}
})
class Data extends QModel<IData> {
declare items: (Content | Metadata)[];
}
// Runtime: data.type === 'content' → instantiates Content
// data.type === 'metadata' → instantiates Metadata✅ Union types with custom type guard function:
For complex discrimination logic, use a custom function.
CRITICAL: The function MUST return one of the types declared in the array. If you declare 5 types, every code path must return one of those 5 types.
@Quick({
items: [Content, Metadata], // 2 types declared
}, {
discriminators: {
// Custom function: MUST return Content or Metadata (the declared types)
items: (data) => {
// Check data structure to determine type
if ('text' in data) return Content;
if ('tags' in data) return Metadata;
// Fallback: return first type (Content)
// NEVER return undefined - always return one of the declared types
return Content;
}
}
})✅ Union types with 5+ types:
If you declare multiple types, EVERY branch must return one of them.
@Quick({
// Declare ALL 5 possible types
items: [TypeA, TypeB, TypeC, TypeD, TypeE],
}, {
discriminators: {
items: (data) => {
// Each branch MUST return one of the 5 declared types
if (data.kind === 'a') return TypeA;
if (data.kind === 'b') return TypeB;
if (data.kind === 'c') return TypeC;
if (data.kind === 'd') return TypeD;
if (data.kind === 'e') return TypeE;
// Fallback: MUST be one of the declared types
return TypeA; // Default to first type
}
}
})✅ Union types with object configuration:
For explicit mapping between discriminator values and types.
@Quick({
items: [Content, Metadata],
}, {
discriminators: {
items: {
field: 'type', // Field name to check
mapping: {
'content': Content, // Explicit mapping
'metadata': Metadata
}
}
}
})✅ Customizing Transformers, Serializers & Mockers:
For full control over the lifecycle of your data.
@Quick({
// Define base types
sku: (val: any) => `ITEM-${val}`, // Inline transformer (fallback to String for mocks)
timestamp: Date
}, {
// 1. TRANSFORMERS (Input -> Model)
// Override default deserialization logic
transformers: {
// Convert seconds -> Date object
timestamp: (val: number) => new Date(val * 1000)
},
// 2. SERIALIZERS (Model -> Output)
// Override default serialization logic (toInterface)
serializers: {
// Convert Date object -> seconds
timestamp: (val: Date) => Math.floor(val.getTime() / 1000)
},
// 3. MOCKERS (Tests -> Model)
// Define how to generate fake data for this field
// Critical for custom transformers where automatic inference fails
mockers: {
// Generate '1234' so transformer produces 'ITEM-1234'
sku: () => faker.string.alphanumeric(4)
}
})
class Product extends QModel<IProduct> {
declare sku: string;
declare timestamp: Date;
}Remarks
Alias option — centralised key remapping:
Use alias in the options object to map model property names to external API keys. This is a centralised alternative to placing @QAlias on each individual property. Both approaches have identical runtime behaviour: alias keys are remapped on input and restored on serialize() output.
// API sends/expects snake_case; model uses camelCase internally.
@Quick({}, { alias: { firstName: 'first_name', lastName: 'last_name' } })
class User extends QModel<IUser> {
declare firstName: string;
declare lastName: string;
}
const user = new User({ first_name: 'Alice', last_name: 'Smith' });
user.firstName; // 'Alice' ✅ camelCase internally
user.$qSerialize(); // { first_name: 'Alice', last_name: 'Smith' } ✅ alias keys in output⚠️ TypeScript limitation: due to experimentalDecorators: true, the return type of $qSerialize() cannot reflect alias keys at compile time through the decorator alone. To get full IDE autocomplete on alias keys, also pass a literal alias map as the second generic of QModel:
type IUserAliases = { firstName: 'first_name'; lastName: 'last_name' };
@Quick({}, { alias: { firstName: 'first_name', lastName: 'last_name' } })
class User extends QModel<IUser, IUserAliases> { ... }
user.$qSerialize().first_name; // ✅ typed correctly — no cast neededWithout the second generic, $qSerialize() still emits alias keys at runtime — only the static type is imprecise.
Why Set/Map need type mapping:
The backend sends both as arrays:
tags: ["a", "b"]→ Array or Set? Cannot determinemetadata: [["k", "v"]]→ Array or Map? Cannot determine
Without explicit type mapping, the decorator cannot know the developer's intent. All special types MUST be explicitly declared - no automatic detection.
Discriminators for union types:
JavaScript has no runtime type information. When an array can contain different types (union types), you MUST provide a discriminator to determine the correct type at runtime:
- String discriminator: Field name whose value matches constructor name
- Function discriminator: Custom logic returning the correct constructor
- Object discriminator: Explicit field + mapping configuration
Without discriminators, QuickModel uses the first type in the array as fallback.
See
@QTypefor per-property decoration (supports TypeScript metadata for!syntax)- IQAdvancedOptions for discriminator configuration
Throws
If unknownPropertyPolicy or another advanced-option key is accidentally passed as the first argument (misconfiguration guard).