Careeroza
Typed Event Names
Events Generated From a Model
Many APIs use event names like nameChanged or order:created. Template literal types can generate these names from a model and connect each name to the right payload.
Example
type Order = { status: "PLACED" | "SHIPPED"; total: number; address: string };
type ChangeEvents<T> = { [K in keyof T as `${string & K}Changed`]: (newValue: T[K], oldValue: T[K]) => void };
function watch<T extends object>(initial: T, handlers: Partial<ChangeEvents<T>>) {
const state = { ...initial };
return {
set<K extends keyof T>(key: K, value: T[K]) {
const old = state[key];
state[key] = value;
const handler = (handlers as Record<string, ((n: T[K], o: T[K]) => void) | undefined>)[`${String(key)}Changed`];
handler?.(value, old);
},
};
}
const order = watch<Order>({ status: "PLACED", total: 999, address: "Hyderabad" }, {
statusChanged: (now, before) => console.log(`status: ${before} -> ${now}`),
totalChanged: (now, before) => console.log(`total: ${before} -> ${now} (diff ${now - before})`),
});
order.set("status", "SHIPPED");
order.set("total", 1099);
order.set("address", "Secunderabad"); // no handler registeredOutput
status: PLACED -> SHIPPED total: 999 -> 1099 (diff 100)
Namespaced Events
Example
An event name like "order:shipped" is checked against the allowed entity and action lists (TS2345 for a typo).
type Entity = "order" | "payment" | "user";
type Action = "created" | "updated" | "deleted";
type DomainEvent = `${Entity}:${Action}`;
function publish(event: DomainEvent, id: string): void {
console.log(event, id);
}
publish("order:created", "OD-1");
publish("payment:refunded", "PAY-9");Compiler output (tsc)
Line 10: error TS2345: Argument of type '"payment:refunded"' is not assignable to parameter of type '"order:created" | "order:updated" | "order:deleted" | "payment:created" | "payment:updated" | "payment:deleted" | "user:created" | "user:updated" | "user:deleted"'.