Skip to content

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. @Quick wraps 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:

typescript
@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):

typescript
@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):

typescript
@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:

typescript
@Quick({ dates: [Date, undefined, null] })
class User extends QModel<IUser> {
  dates?: (Date | undefined | null)[]; // Transforms strings to Date, preserves undefined/null
}

✅ Custom transformer functions:

typescript
@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:

typescript
// 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:

typescript
@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:

typescript
@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.

typescript
// 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.

typescript
@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.

typescript
@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.

typescript
@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.

typescript
@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.

typescript
// 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:

typescript
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 needed

Without 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 determine
  • metadata: [["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 ​

  • @QType for 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).