Computed Fields with @QComputed
Add derived properties to your models that are automatically included in serialization using the @QComputed decorator.
Problem
You have a User model with firstName and lastName, and you want fullName available in the serialized object without calculating it manually everywhere.
Basic Usage
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;
// ✅ Included in toJSON() and serialize()
@QComputed()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
// ✅ Also included
@QComputed()
get formattedSalary(): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: this.currency,
}).format(this.salary);
}
// ❌ NOT included in serialization (no @QComputed)
get initials(): string {
return `${this.firstName[0]}.${this.lastName[0]}.`;
}
}
const user = User.create({
firstName: 'Alice',
lastName: 'Smith',
salary: 75000,
currency: 'USD',
});
console.log(user.fullName); // 'Alice Smith'
console.log(user.initials); // 'A.S.' (available on instance but not serialized)
console.log(user.formattedSalary); // '$75,000.00'
const plain = user.$qSerialize();
console.log(plain.fullName); // 'Alice Smith' ✅ included
console.log(plain.formattedSalary); // '$75,000.00' ✅ included
console.log(plain.initials); // undefined ❌ not included (no @QComputed)Product Model with Calculations
typescript
interface IProduct {
name: string;
priceNet: number;
vatRate: number; // Percentage, e.g. 20
discountRate: number; // Percentage, e.g. 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} (was $${this.priceGross})`
: `$${this.priceGross}`;
}
}
const product = Product.create({
name: 'Pro Laptop',
priceNet: 1000,
vatRate: 20,
discountRate: 10,
});
const plain = product.$qSerialize();
console.log(plain.priceNet); // 1000
console.log(plain.vatAmount); // 200
console.log(plain.priceGross); // 1200
console.log(plain.discount); // 120
console.log(plain.finalPrice); // 1080
console.log(plain.priceLabel); // '$1080 (was $1200)'Person Model with Age and Status
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: 'Administrator', user: 'User', guest: 'Guest' };
return labels[this.role];
}
@QComputed()
get summary(): string {
return `${this.fullName}, ${this.age} years old (${this.displayRole})`;
}
}
const person = Person.create({
firstName: 'Alice',
lastName: 'Smith',
birthDate: '1990-06-15',
role: 'admin',
});
console.log(person.summary); // 'Alice Smith, 35 years old (Administrator)'
const serialized = person.$qSerialize();
// {
// firstName: 'Alice', lastName: 'Smith',
// birthDate: '1990-06-15T00:00:00.000Z', role: 'admin',
// fullName: 'Alice Smith', age: 35, isAdult: true,
// displayRole: 'Administrator',
// summary: 'Alice Smith, 35 years old (Administrator)'
// }Combining @QComputed with @QAlias and @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: 'Unit price must be positive',
})
declare unitPrice: number;
@QAlias('quantity')
@QRule({
predicate: (val: number) => val >= 1,
message: 'Quantity must be at least 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.47Best Practices
typescript
// ✅ Good: pure calculations based on model properties
@QComputed()
get total(): number {
return this.price * this.quantity;
}
// ❌ Avoid: side effects in getters
@QComputed()
get total(): number {
console.log('computing...'); // ❌ side effect
this.lastCalculated = Date.now(); // ❌ mutation
return this.price * this.quantity;
}
// ❌ Avoid: async operations in @QComputed getters
// Getters must be synchronousNext Steps
- Mocks & Testing - Generate test data with
mock() - Validation - Combine with
@QRule