Campos Computados con @QComputed
Añade propiedades derivadas a tus modelos que se incluyen automáticamente en la serialización con el decorador @QComputed.
Problema
Tienes un modelo User con firstName y lastName, y quieres que fullName esté disponible en el objeto serializado sin tener que calcularlo manualmente en cada sitio.
Uso Básico
typescript
import { QModel, Quick, QComputed } from 'quickmodel';
interface IUser {
firstName: string;
lastName: string;
salary: number;
currency: string;
}
@Quick()
class User extends QModel<IUser> {
declare firstName: string;
declare lastName: string;
declare salary: number;
declare currency: string;
// ✅ Se incluye en toJSON() y serialize()
@QComputed()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
// ✅ También se incluye
@QComputed()
get formattedSalary(): string {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: this.currency,
}).format(this.salary);
}
// ❌ NO se incluye en la serialización (sin @QComputed)
get initials(): string {
return `${this.firstName[0]}.${this.lastName[0]}.`;
}
}
const user = User.create({
firstName: 'María',
lastName: 'García',
salary: 45000,
currency: 'EUR',
});
console.log(user.fullName); // 'María García'
console.log(user.initials); // 'M.G.' (disponible en instancia, pero no en serialización)
console.log(user.formattedSalary); // '45.000,00 €'
const plain = user.$qSerialize();
console.log(plain.fullName); // 'María García' ✅ incluido
console.log(plain.formattedSalary); // '45.000,00 €' ✅ incluido
console.log(plain.initials); // undefined ❌ no incluido (sin @QComputed)Modelo de Producto con Cálculos
typescript
interface IProduct {
name: string;
priceNet: number;
vatRate: number; // Porcentaje, ej: 21
discountRate: number; // Porcentaje, ej: 10
}
@Quick()
class Product extends QModel<IProduct> {
declare name: string;
declare priceNet: number;
declare vatRate: number;
declare discountRate: number;
@QComputed()
get vatAmount(): number {
return Number((this.priceNet * (this.vatRate / 100)).toFixed(2));
}
@QComputed()
get priceGross(): number {
return Number((this.priceNet + this.vatAmount).toFixed(2));
}
@QComputed()
get discount(): number {
return Number((this.priceGross * (this.discountRate / 100)).toFixed(2));
}
@QComputed()
get finalPrice(): number {
return Number((this.priceGross - this.discount).toFixed(2));
}
@QComputed()
get priceLabel(): string {
return this.discountRate > 0
? `${this.finalPrice}€ (antes ${this.priceGross}€)`
: `${this.priceGross}€`;
}
}
const product = Product.create({
name: 'Laptop Pro',
priceNet: 1000,
vatRate: 21,
discountRate: 10,
});
const plain = product.$qSerialize();
console.log(plain.priceNet); // 1000
console.log(plain.vatAmount); // 210
console.log(plain.priceGross); // 1210
console.log(plain.discount); // 121
console.log(plain.finalPrice); // 1089
console.log(plain.priceLabel); // '1089€ (antes 1210€)'Modelo de Persona con Edad y Estado
typescript
interface IPerson {
firstName: string;
lastName: string;
birthDate: string;
role: 'admin' | 'user' | 'guest';
}
@Quick({ birthDate: Date })
class Person extends QModel<IPerson> {
declare firstName: string;
declare lastName: string;
declare birthDate: Date;
declare role: 'admin' | 'user' | 'guest';
@QComputed()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
@QComputed()
get age(): number {
const today = new Date();
let age = today.getFullYear() - this.birthDate.getFullYear();
const monthDiff = today.getMonth() - this.birthDate.getMonth();
if (
monthDiff < 0 ||
(monthDiff === 0 && today.getDate() < this.birthDate.getDate())
) {
age--;
}
return age;
}
@QComputed()
get isAdult(): boolean {
return this.age >= 18;
}
@QComputed()
get displayRole(): string {
const labels = {
admin: 'Administrador',
user: 'Usuario',
guest: 'Invitado',
};
return labels[this.role];
}
@QComputed()
get summary(): string {
return `${this.fullName}, ${this.age} años (${this.displayRole})`;
}
}
const person = Person.create({
firstName: 'Carlos',
lastName: 'Ruiz',
birthDate: '1990-06-15',
role: 'admin',
});
console.log(person.summary); // 'Carlos Ruiz, 35 años (Administrador)'
const serialized = person.$qSerialize();
// {
// firstName: 'Carlos', lastName: 'Ruiz',
// birthDate: '1990-06-15T00:00:00.000Z',
// role: 'admin',
// fullName: 'Carlos Ruiz',
// age: 35,
// isAdult: true,
// displayRole: 'Administrador',
// summary: 'Carlos Ruiz, 35 años (Administrador)'
// }Combinar @QComputed con @QAlias y @QRule
typescript
interface IOrderAPI {
unit_price: number;
quantity: number;
discount_pct: number;
}
@Quick()
class Order extends QModel<IOrderAPI> {
@QAlias('unit_price')
@QRule({
predicate: (val: number) => val > 0,
message: 'El precio unitario debe ser positivo',
})
declare unitPrice: number;
@QAlias('quantity')
@QRule({
predicate: (val: number) => val >= 1,
message: 'La cantidad debe ser al menos 1',
})
declare quantity: number;
@QAlias('discount_pct')
declare discountPct: number;
@QComputed()
get subtotal(): number {
return Number((this.unitPrice * this.quantity).toFixed(2));
}
@QComputed()
get discountAmount(): number {
return Number(((this.subtotal * this.discountPct) / 100).toFixed(2));
}
@QComputed()
get total(): number {
return Number((this.subtotal - this.discountAmount).toFixed(2));
}
}
const order = Order.create({
unit_price: 29.99,
quantity: 3,
discount_pct: 5,
});
console.log(order.subtotal); // 89.97
console.log(order.discountAmount); // 4.5
console.log(order.total); // 85.47
const plain = order.$qSerialize();
// Claves de salida = aliases (snake_case)
// {
// unit_price: 29.99, quantity: 3, discount_pct: 5,
// subtotal: 89.97, discountAmount: 4.5, total: 85.47
// }
// ↑ Los campos @QComputed NO tienen alias → usan su nombre de propiedadCampos Computados con Modelos Anidados
typescript
interface ITeam {
name: string;
members: string[];
}
interface IProject {
title: string;
status: 'planning' | 'active' | 'done';
budget: number;
spent: number;
}
@Quick()
class Project extends QModel<IProject> {
declare title: string;
declare status: 'planning' | 'active' | 'done';
declare budget: number;
declare spent: number;
@QComputed()
get remaining(): number {
return this.budget - this.spent;
}
@QComputed()
get spentPercent(): number {
return Math.round((this.spent / this.budget) * 100);
}
@QComputed()
get isOverBudget(): boolean {
return this.spent > this.budget;
}
@QComputed()
get statusLabel(): string {
const labels = {
planning: 'En planificación',
active: 'En curso',
done: 'Completado',
};
return labels[this.status];
}
}
const project = Project.create({
title: 'Rediseño Web',
status: 'active',
budget: 10000,
spent: 7500,
});
console.log(project.remaining); // 2500
console.log(project.spentPercent); // 75
console.log(project.isOverBudget); // false
console.log(project.statusLabel); // 'En curso'Mejores Prácticas
typescript
// ✅ Bien: cálculos puros basados en propiedades del modelo
@QComputed()
get total(): number {
return this.price * this.quantity;
}
// ❌ Evitar: efectos secundarios en getters
@QComputed()
get total(): number {
console.log('calculando...'); // ❌ side effect
this.lastCalculated = Date.now(); // ❌ mutación
return this.price * this.quantity;
}
// ❌ Evitar: llamadas async en getters @QComputed
// Los getters deben ser síncronosPróximos Pasos
- Mocks y Testing - Genera datos de prueba con
mock() - Validación - Combina con
@QRule