Custom Transformers
QuickModel allows you to create custom transformers for your own types or to override built-in behavior.
Creating a Custom Transformer
A transformer is a class that implements the transformation logic for a specific type.
Basic Structure
import { IQTransformer } from 'quickmodel/types';
class MyCustomTransformer implements IQTransformer<SerializedType, MyType> {
// Transform from JSON to runtime type
deserialize(value: SerializedType): MyType {
// Your transformation logic
return new MyType(value);
}
// Transform from runtime type back to JSON
serialize(value: MyType): SerializedType {
// Your serialization logic
return value.toJSON();
}
}Inline Transformer Objects
Sometimes you don't need to create a full class for a simple or one-off transformation. QuickModel allows you to pass object literals directly to the @Quick decorator as long as they implement the IQTransformer interface (serialize and deserialize methods).
This is ideal for reducers, quick formatters, or model-specific logic without polluting the global registry.
// Define the transformer as a constant object
const ReverseString = {
// From JSON to Model
deserialize(value: string): string {
return value && value.split('').reverse().join('');
},
// From Model to JSON
serialize(value: string): string {
return value.split('').reverse().join('');
},
};
@Quick({
// Use it directly
secretCode: ReverseString,
})
class SpyMessage extends QModel<IMessage> {
declare secretCode: string;
}Example: Money Type
Let's create a custom Money type with currency support:
// 1. Define your custom type
class Money {
constructor(
public amount: number,
public currency: string
) {}
toString() {
return `${this.amount} ${this.currency}`;
}
}
// 2. Define the serialization format
interface IMoneyJSON {
amount: number;
currency: string;
}
// 3. Create the transformer
import { IQTransformer } from 'quickmodel/types';
class MoneyTransformer implements IQTransformer<IMoneyJSON, Money> {
deserialize(value: IMoneyJSON): Money {
if (!value || typeof value !== 'object') {
throw new Error('Invalid money format');
}
return new Money(value.amount, value.currency);
}
serialize(value: Money): IMoneyJSON {
return {
amount: value.amount,
currency: value.currency,
};
}
}
// 4. Register the transformer
import { QTransformerRegistry } from 'quickmodel/advanced';
QTransformerRegistry.register(Money, new MoneyTransformer());
// 5. Use it in your models
interface IProduct {
id: string;
name: string;
price: IMoneyJSON;
}
@Quick({ price: Money })
class Product extends QModel<IProduct> {
declare id: string;
declare name: string;
declare price: Money;
}
// 6. Test it
const product = new Product({
id: '1',
name: 'Laptop',
price: { amount: 999.99, currency: 'USD' },
});
console.log(product.price instanceof Money); // true
console.log(product.price.toString()); // '999.99 USD'
const plain = product.$qSerialize();
console.log(plain.price); // { amount: 999.99, currency: 'USD' }Example: Color Type
Create a custom color type with hex/RGB conversion:
class Color {
constructor(
public red: number,
public green: number,
public blue: number
) {}
toHex(): string {
const toHex = (num: number) => num.toString(16).padStart(2, '0');
return `#${toHex(this.red)}${toHex(this.green)}${toHex(this.blue)}`;
}
static fromHex(hex: string): Color {
const red = parseInt(hex.slice(1, 3), 16);
const green = parseInt(hex.slice(3, 5), 16);
const blue = parseInt(hex.slice(5, 7), 16);
return new Color(red, green, blue);
}
}
class ColorTransformer implements IQTransformer<string, Color> {
deserialize(value: string): Color {
if (typeof value !== 'string' || !value.startsWith('#')) {
throw new Error(
'Invalid color format. Expected hex string like #FF0000'
);
}
return Color.fromHex(value);
}
serialize(value: Color): string {
return value.toHex();
}
}
QTransformerRegistry.register(Color, new ColorTransformer());
// Usage
@Quick({ backgroundColor: Color })
class Theme extends QModel<ITheme> {
declare backgroundColor: Color;
}
const theme = new Theme({ backgroundColor: '#FF5733' });
console.log(theme.backgroundColor.r); // 255
console.log(theme.backgroundColor.toHex()); // '#FF5733'Example: Coordinate Type
Geographic coordinates with validation:
class Coordinate {
constructor(
public latitude: number,
public longitude: number
) {
if (latitude < -90 || latitude > 90) {
throw new Error('Latitude must be between -90 and 90');
}
if (longitude < -180 || longitude > 180) {
throw new Error('Longitude must be between -180 and 180');
}
}
distanceTo(other: Coordinate): number {
// Haversine formula implementation
// ...
}
}
interface ICoordinateJSON {
lat: number;
lng: number;
}
class CoordinateTransformer implements IQTransformer<
ICoordinateJSON,
Coordinate
> {
deserialize(value: ICoordinateJSON): Coordinate {
if (!value || typeof value !== 'object') {
throw new Error('Invalid coordinate format');
}
return new Coordinate(value.lat, value.lng);
}
serialize(value: Coordinate): ICoordinateJSON {
return {
lat: value.latitude,
lng: value.longitude,
};
}
}
QTransformerRegistry.register(Coordinate, new CoordinateTransformer());
// Usage
@Quick({ location: Coordinate })
class Store extends QModel<IStore> {
declare name: string;
declare location: Coordinate;
}
const store = new Store({
name: 'Main Store',
location: { lat: 40.7128, lng: -74.006 },
});
console.log(store.location instanceof Coordinate); // trueOverriding Built-in Transformers
You can override built-in transformers if you need custom behavior:
// Custom Date transformer that handles multiple formats
class CustomDateTransformer implements IQTransformer<string, Date> {
deserialize(value: string): Date {
// Support multiple date formats
if (value.includes('/')) {
// Handle MM/DD/YYYY
const [month, day, year] = value.split('/');
return new Date(+year, +month - 1, +day);
}
// Default ISO format
return new Date(value);
}
serialize(value: Date): string {
return value.toISOString();
}
}
// Override the built-in Date transformer
QTransformerRegistry.register(Date, new CustomDateTransformer());Handling Null and Undefined
Transformers should handle null and undefined gracefully:
class MoneyTransformer implements IQTransformer<IMoneyJSON | null, Money> {
deserialize(value: IMoneyJSON | null): Money | null {
if (value === null || value === undefined) {
return null;
}
return new Money(value.amount, value.currency);
}
serialize(value: Money | null): IMoneyJSON | null {
if (value === null || value === undefined) {
return null;
}
return {
amount: value.amount,
currency: value.currency,
};
}
}Array Support
To support arrays of your custom type, the transformer works automatically:
@Quick({
price: Money, // Single Money
prices: [Money], // Array of Money
})
class Product extends QModel<IProduct> {
declare price: Money;
declare prices: Money[];
}
const product = new Product({
price: { amount: 99.99, currency: 'USD' },
prices: [
{ amount: 99.99, currency: 'USD' },
{ amount: 79.99, currency: 'EUR' },
],
});
console.log(product.prices[0] instanceof Money); // trueValidation in Transformers
Add validation logic in your transformers:
class EmailTransformer implements IQTransformer<string, string> {
private emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
deserialize(value: string): string {
if (!this.emailRegex.test(value)) {
throw new Error(`Invalid email format: ${value}`);
}
return value.toLowerCase(); // Normalize to lowercase
}
serialize(value: string): string {
return value;
}
}
// Create a custom Email type
class Email extends String {}
QTransformerRegistry.register(Email, new EmailTransformer());
@Quick({ email: Email })
class User extends QModel<IUser> {
declare email: string;
}
// This will throw an error
const user = new User({ email: 'invalid-email' }); // Error!Best Practices
1. Validate Input
Always validate input in deserialize():
deserialize(value: any): MyType {
if (!value || typeof value !== 'object') {
throw new Error('Invalid input format');
}
// ... rest of transformation
}2. Handle Edge Cases
Consider null, undefined, empty strings, etc.:
deserialize(value: any): MyType | null {
if (value === null || value === undefined) {
return null;
}
if (value === '') {
return null; // or throw error
}
// ... transformation logic
}3. Provide Clear Error Messages
Help users debug transformation issues:
deserialize(value: any): MyType {
if (!value.requiredField) {
throw new Error(
`Missing required field 'requiredField' in ${JSON.stringify(value)}`
);
}
// ...
}4. Keep Transformers Pure
Transformers should be stateless and deterministic:
// ✅ Good - pure function
deserialize(value: IMoneyJSON): Money {
return new Money(value.amount, value.currency);
}
// ❌ Bad - stateful
private counter = 0;
deserialize(value: IMoneyJSON): Money {
this.counter++; // Side effect!
return new Money(value.amount, value.currency);
}5. Make Transformations Reversible
Ensure serialize(deserialize(x)) returns equivalent data:
const original = { amount: 99.99, currency: 'USD' };
const money = transformer.deserialize(original);
const serialized = transformer.serialize(money);
console.log(JSON.stringify(original) === JSON.stringify(serialized)); // trueNext Steps
- Transformers - See all built-in transformers
- Nested Models - Combine custom types with nesting
- Examples - Real-world custom type examples