@dto
Class decorator that adds reactivity, cloning, filling and serialization to a class. The full reference for the decorator and the helper functions that operate on DTO instances lives on this page.
Importing
import { dto } from '@basmilius/http-client';Usage
DTOs use private fields with a # prefix and expose them through getter/setter pairs.
import { dto } from '@basmilius/http-client';
@dto
export class UserDto {
get id(): string {
return this.#id;
}
set id(value: string) {
this.#id = value;
}
get email(): string {
return this.#email;
}
set email(value: string) {
this.#email = value;
}
#id: string;
#email: string;
constructor(id: string, email: string) {
this.#id = id;
this.#email = email;
}
}
const user = new UserDto('user-1', 'a@example.com');
const clone = user.clone();
const json = user.toJSON();What @dto does
- Walks the prototype chain via
getPrototypeChainand freezes the descriptor map on the prototype under a private symbol. - Stores the property names and the class name on the prototype so the helpers can introspect every instance.
- Replaces the class with a
Proxy. The proxy'sconstructtrap returns acustomRefthat proxies the real instance — every property read tracks, every write triggers. - Registers the class in a global
DTO_CLASS_MAPkeyed byclazz.name.deserializeuses this map to re-hydrate instances by name. - Adds
clone(),fill()andtoJSON()to the prototype. - Overrides
Symbol.hasInstancesoinstance instanceof Clazzmatches via the internal[NAME]symbol — proxied instances still pass the check. - Validates that the parent class is not also decorated with
@dto, throwing during decoration when the constraint is violated.
Limitations
- A
@dtoclass cannot extend another@dtoclass. The decorator throws an error during validation when a parent class also has aNAMEsymbol on its prototype. - Properties without a getter/setter pair are invisible to
clone(),fill()andtoJSON(). Always model fields as private (#field) plus matching accessors. - Do not use the decorator inline inside Vue Single File Components — most SFC pipelines (esbuild) do not support legacy decorators in
<script>blocks. Define the class in a.tsfile and import it.
Type signature
declare function dto<T extends Constructor>(clazz: T): T;Helpers
The package exposes a set of helper functions that act on DTO instances. They are exported alongside dto from the package root.
import {
assertDto,
cloneDto,
deserialize,
executeIfDtoDirtyAndMarkClean,
isDto,
isDtoClean,
isDtoDirty,
markDtoClean,
markDtoDirty,
serialize
} from '@basmilius/http-client';assertDto
Asserts that a value is a DTO instance. Throws when it is not.
declare function assertDto(obj: unknown): asserts obj is DtoInstance<never>;import { assertDto } from '@basmilius/http-client';
function rename(maybeUser: unknown, name: string): void {
assertDto(maybeUser);
(maybeUser as UserDto).fullName = name;
}The error message is @dto assert given object is not a class decorated with @Dto.
cloneDto
Clones a DTO. Wraps instance.clone() with a built-in assertDto so the input is checked at runtime.
declare function cloneDto<T>(obj: T): T;import { cloneDto } from '@basmilius/http-client';
const draft = cloneDto(user);isDto
Type guard predicate for DTO instances. Tests for the presence of the internal NAME symbol on the prototype.
declare function isDto(obj: unknown): obj is DtoInstance<unknown>;if (isDto(value)) {
value.fill(payload);
}isDtoClean
Returns true when the DTO has not been modified since the last markDtoClean (or its construction). Asserts the input is a DTO.
declare function isDtoClean(obj: unknown): boolean;isDtoDirty
Returns true when the DTO has been modified since the last markDtoClean. Asserts the input is a DTO.
declare function isDtoDirty(obj: unknown): boolean;markDtoClean
Marks a DTO clean and recurses into its tracked children, marking each dirty descendant clean as well. Triggers reactivity for the dirty flag, so reactive consumers re-render.
declare function markDtoClean(obj: unknown): void;markDtoDirty
Marks a DTO dirty and propagates upwards through the PARENT chain — child writes always mark their containing DTO dirty too.
declare function markDtoDirty(obj: unknown, key?: string | number): void;The internal write traps already call markDtoDirty for you, so the helper is mostly useful when synthesising changes from outside the proxy boundary.
executeIfDtoDirtyAndMarkClean
Asynchronous helper that runs fn only when the DTO is dirty and marks it clean once fn resolves. Common at the boundary between a form and its persistence layer.
declare function executeIfDtoDirtyAndMarkClean<T, R = void>(
obj: T,
fn: (dto: T & DtoInstance<T>) => Promise<R>
): Promise<void>;await executeIfDtoDirtyAndMarkClean(user, async (dirty) => {
await userService.update(dirty);
});If the DTO is clean, fn is not called and the helper resolves immediately.
serialize
Serialises a value (DTO, plain object, array, primitive, Luxon DateTime) to a JSON string. Each DTO is tagged with its class name and constructor arguments, so deserialize can reconstruct the exact graph.
declare function serialize(obj: unknown): string;import { serialize } from '@basmilius/http-client';
const json = serialize(user);
localStorage.setItem('user', json);deserialize
Restores a value previously produced by serialize. DTOs are recreated by looking up their class name in the global DTO map, so the originating class must be imported (and therefore registered) in the consuming runtime.
declare function deserialize(serialized: string): unknown;import { deserialize } from '@basmilius/http-client';
import { UserDto } from './dto/UserDto';
const restored = deserialize(localStorage.getItem('user') ?? '') as UserDto;When a DTO references the same nested instance multiple times, deserialize keeps a per-call cache keyed by the per-instance UUID minted by serialize, so the restored graph preserves identity.
See also
- DTO pattern guide — practical walkthrough.
Paginated,BlobResponse,RequestError,ValidationError— the DTOs that ship with the package.useDtoForm— Vue composable in@basmilius/commonthat drives a DTO through a clone-based form lifecycle.