Transformadores Personalizados
QuickModel te permite crear transformadores personalizados para tus propios tipos o para sobrescribir el comportamiento integrado.
Crear un Transformador Personalizado
Un transformador es una clase que implementa la lógica de transformación para un tipo específico.
Estructura Básica
import { IQTransformer } from 'quickmodel/types';
class MyCustomTransformer implements IQTransformer<SerializedType, MyType> {
// Transformar de JSON a tipo runtime
deserialize(value: SerializedType): MyType {
// Tu lógica de transformación
return new MyType(value);
}
// Transformar de tipo runtime de vuelta a JSON
serialize(value: MyType): SerializedType {
// Tu lógica de serialización
return value.toJSON();
}
}Objetos Transformadores en Línea
A veces no necesitas crear una clase completa para una transformación simple o de un solo uso. QuickModel permite pasar objetos literales directamente al decorador @Quick siempre que implementen la interfaz IQTransformer (métodos serialize y deserialize).
Esto es ideal para reducers, formateadores rápidos o lógica específica de un modelo sin contaminar el registro global.
// Define el transformador como un objeto constante
const ReverseString = {
// De JSON a Modelo
deserialize(value: string): string {
return value && value.split('').reverse().join('');
},
// De Modelo a JSON
serialize(value: string): string {
return value.split('').reverse().join('');
},
};
@Quick({
// Úsalo directamente
secretCode: ReverseString,
})
class SpyMessage extends QModel<IMessage> {
declare secretCode: string;
}Ejemplo: Tipo Money
Vamos a crear un tipo Money personalizado con soporte de moneda:
// 1. Define tu tipo personalizado
class Money {
constructor(
public amount: number,
public currency: string
) {}
toString() {
return `${this.amount} ${this.currency}`;
}
}
// 2. Define el formato de serialización
interface IMoneyJSON {
amount: number;
currency: string;
}
// 3. Crea el transformador
import { IQTransformer } from 'quickmodel/types';
class MoneyTransformer implements IQTransformer<IMoneyJSON, Money> {
deserialize(value: IMoneyJSON): Money {
if (!value || typeof value !== 'object') {
throw new Error('Formato de dinero inválido');
}
return new Money(value.amount, value.currency);
}
serialize(value: Money): IMoneyJSON {
return {
amount: value.amount,
currency: value.currency,
};
}
}
// 4. Registra el transformador
import { QTransformerRegistry } from 'quickmodel/advanced';
QTransformerRegistry.register(Money, new MoneyTransformer());
// 5. Úsalo en tus modelos
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. Pruébalo
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' }Ejemplo: Tipo Color
Crea un tipo de color personalizado con conversión hex/RGB:
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(
'Formato de color inválido. Se esperaba string hex como #FF0000'
);
}
return Color.fromHex(value);
}
serialize(value: Color): string {
return value.toHex();
}
}
QTransformerRegistry.register(Color, new ColorTransformer());
// Uso
@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'Ejemplo: Tipo Coordenada
Coordenadas geográficas con validación:
class Coordinate {
constructor(
public latitude: number,
public longitude: number
) {
if (latitude < -90 || latitude > 90) {
throw new Error('La latitud debe estar entre -90 y 90');
}
if (longitude < -180 || longitude > 180) {
throw new Error('La longitud debe estar entre -180 y 180');
}
}
distanceTo(other: Coordinate): number {
// Implementación de fórmula Haversine
// ...
}
}
interface ICoordinateJSON {
lat: number;
lng: number;
}
class CoordinateTransformer implements IQTransformer<
ICoordinateJSON,
Coordinate
> {
deserialize(value: ICoordinateJSON): Coordinate {
if (!value || typeof value !== 'object') {
throw new Error('Formato de coordenada inválido');
}
return new Coordinate(value.lat, value.lng);
}
serialize(value: Coordinate): ICoordinateJSON {
return {
lat: value.latitude,
lng: value.longitude,
};
}
}
QTransformerRegistry.register(Coordinate, new CoordinateTransformer());
// Uso
@Quick({ location: Coordinate })
class Store extends QModel<IStore> {
declare name: string;
declare location: Coordinate;
}
const store = new Store({
name: 'Tienda Principal',
location: { lat: 40.7128, lng: -74.006 },
});
console.log(store.location instanceof Coordinate); // trueSobrescribir Transformadores Integrados
Puedes sobrescribir transformadores integrados si necesitas comportamiento personalizado:
// Transformador de Fecha personalizado que maneja múltiples formatos
class CustomDateTransformer implements IQTransformer<string, Date> {
deserialize(value: string): Date {
// Soportar múltiples formatos de fecha
if (value.includes('/')) {
// Manejar MM/DD/YYYY
const [month, day, year] = value.split('/');
return new Date(+year, +month - 1, +day);
}
// Formato ISO por defecto
return new Date(value);
}
serialize(value: Date): string {
return value.toISOString();
}
}
// Sobrescribir el transformador Date integrado
QTransformerRegistry.register(Date, new CustomDateTransformer());Manejo de Null y Undefined
Los transformadores deben manejar null y undefined de manera elegante:
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,
};
}
}Soporte de Arrays
Para soportar arrays de tu tipo personalizado, el transformador funciona automáticamente:
@Quick({
price: Money, // Money único
prices: [Money], // Array de 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); // trueValidación en Transformadores
Añade lógica de validación en tus transformadores:
class EmailTransformer implements IQTransformer<string, string> {
private emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
deserialize(value: string): string {
if (!this.emailRegex.test(value)) {
throw new Error(`Formato de email inválido: ${value}`);
}
return value.toLowerCase(); // Normalizar a minúsculas
}
serialize(value: string): string {
return value;
}
}
// Crear un tipo Email personalizado
class Email extends String {}
QTransformerRegistry.register(Email, new EmailTransformer());
@Quick({ email: Email })
class User extends QModel<IUser> {
declare email: string;
}
// Esto lanzará un error
const user = new User({ email: 'email-invalido' }); // ¡Error!Mejores Prácticas
1. Validar Entrada
Siempre valida la entrada en deserialize():
deserialize(value: any): MyType {
if (!value || typeof value !== 'object') {
throw new Error('Formato de entrada inválido');
}
// ... resto de la transformación
}2. Manejar Casos Extremos
Considera null, undefined, strings vacíos, etc.:
deserialize(value: any): MyType | null {
if (value === null || value === undefined) {
return null;
}
if (value === '') {
return null; // o lanzar error
}
// ... lógica de transformación
}3. Proporcionar Mensajes de Error Claros
Ayuda a los usuarios a depurar problemas de transformación:
deserialize(value: any): MyType {
if (!value.requiredField) {
throw new Error(
`Falta el campo requerido 'requiredField' en ${JSON.stringify(value)}`
);
}
// ...
}4. Mantener Transformadores Puros
Los transformadores deben ser sin estado y deterministas:
// ✅ Bien - función pura
deserialize(value: IMoneyJSON): Money {
return new Money(value.amount, value.currency);
}
// ❌ Mal - con estado
private counter = 0;
deserialize(value: IMoneyJSON): Money {
this.counter++; // ¡Efecto secundario!
return new Money(value.amount, value.currency);
}5. Hacer Transformaciones Reversibles
Asegura que serialize(deserialize(x)) devuelva datos equivalentes:
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)); // truePróximos Pasos
- Transformadores - Ve todos los transformadores integrados
- Modelos Anidados - Combina tipos personalizados con anidamiento
- Ejemplos - Ejemplos de tipos personalizados del mundo real