Трейты
Трейт - переиспользуемый срез модели: поля, поведение и юниты, которые их связывают. Используйте его, когда несколько моделей делят один и тот же кусок - таймстампы, выбор, мягкое удаление - а копии декларации в каждой модели со временем разъехались бы.
Трейт идентифицируется ссылкой на объект. Никаких строковых имён, которые нужно регистрировать и которые могут столкнуться.
Примеры на этой странице выполняются в активном скоупе - см. Коллекции и экземпляры.
Общие поля
Простейший трейт только объявляет данные. Каждая модель, указавшая его в with, получает поля как свои собственные:
const Timestamped = trait({
data: { createdAt: f.date().indexed("ord") },
});
const Post = staticModel({
with: [Timestamped],
data: { title: f.string() },
});
const post = collection(Post).add({
title: "привет",
createdAt: "2026-08-08T10:00:00.000Z",
});
post.createdAt.value; // Date - обычное поле PostТребования полей и поведения
Трейт может не объявлять, а требовать. requires принимает схемы полей («модель обязана объявить это поле») и сигнатуры fn<...>() («какой-то setup обязан вернуть эту функцию»). setup трейта использует и то и другое через self:
const Selectable = trait({
requires: {
selected: f.boolean(),
canSelect: fn<() => boolean>(),
},
setup(self) {
const toggle = event<void>();
reaction({
on: toggle,
run: () => {
if (!self.canSelect()) return;
self.selected.value = !self.selected.value;
},
});
return { toggle };
},
});
const Todo = staticModel({
with: [Selectable],
data: {
title: f.string(),
selected: f.boolean(false), // закрывает требование поля
},
setup() {
return { canSelect: () => true }; // закрывает требование поведения
},
});
const t = collection(Todo).add({ title: "x" });
await t.toggle(); // событие трейта, доступно на экземпляре
t.selected.value; // trueЧто происходит:
- всё, что трейт объявляет или возвращает, протекает в
selfи на экземпляр - отдельного «пространства имён трейта» нет; - необъявленное требование поля падает при декларации модели;
- нереализованное требование поведения падает после прогона всех setup.
Правило имён
Требовать имя могут многие; объявлять - ровно один. Два объявления - двумя трейтами, трейтом и моделью, двумя возвратами setup - падают сразу:
const A = trait({ data: { count: f.number(0) } });
const B = trait({ data: { count: f.number(0) } });
staticModel({ with: [A, B] });
// Error: declaration collision on "count" - exactly one may declareКомпозиция остаётся читаемой: у каждого члена ровно одно место, которое его определяет.
Обобщённый код над экземплярами
Impl<typeof Trait> типизирует «любой экземпляр модели, реализующей трейт»:
import type { Impl } from "@virentia/core/models";
function selectAll<M extends Impl<typeof Selectable>>(items: readonly M[]) {
for (const item of items) {
if (item.canSelect()) item.toggle();
}
}
selectAll(collection(Todo).items); // ок
selectAll(collection(User).items); // ошибка компиляции - User не реализуетПараметризованные трейты
f.arg(n) оставляет в схеме поля дыру; применение трейта её заполняет. Применённый трейт сохраняет идентичность исходного - обе модели ниже реализуют Keyed:
const Keyed = trait({
data: { code: f.arg(0) },
});
const Product = staticModel({ with: [Keyed(f.string())], data: {} });
const Slot = staticModel({ with: [Keyed(f.number(0))], data: {} });Контракт
function trait(config: {
with?: readonly Trait[];
requires?: Record<string, Field | FnRequirement>;
data?: DataDeclaration; // те же формы, что у data модели
setup?: (self) => members | void;
}): Trait;
type Impl<T extends Trait>; // констрейнт для обобщённого кодаТрейт, пришедший несколькими путями (цепочки with), применяется один раз.
Частые кейсы
Используйте трейты для:
- общих серверных полей: таймстампы, авторство, версионирование;
- общего поведения над данными модели: выбор, разворачивание, dirty-трекинг;
- базы общих членов объединений - между вариантами делятся только поля из трейтов;
- доменных словарей: трейт на способность, модели компонуют то, что умеют.
Связанные разделы
- Поля и данные - что принимают
dataиrequires. - Объединения - почему общие поля должны идти из одного трейта.
- Коллекции и экземпляры - как ведут себя
setupиself.