Типизация схемы
Вся типобезопасность Triggery идёт из одного inline-generic на createTrigger: это TriggerSchema, перечисляющий, какие у триггера события, условия и действия. Этот один тип питает восемь производных типов под капотом, сигнатуры хуков каждого фреймворк-биндинга и proxy именованных хуков. Эта страница ведёт по generic, по производным типам, которые иногда нужно называть явно, и по паттернам, масштабирующимся на большие схемы.
Generic TriggerSchema
Заголовок раздела «Generic TriggerSchema»У TriggerSchema три необязательные карты:
Все три необязательны, но у полезного триггера всегда есть как минимум events — без событий обработчик никогда не запускается. Схемы только с conditions или только с actions легальны, но редки.
Схема и есть контракт. Каждый ключ, каждый payload, каждый аргумент действия типизируется этим одним generic. Переименуй здесь 'new-message' — и TS сломает каждый вызов useEvent(messageTrigger, 'new-message').
Void-payload
Заголовок раздела «Void-payload»События и действия без данных используют void:
void специально обрабатывается по всему API:
Поведение обеспечено небольшим conditional-типом в публичном API: ActionFn<P> = [P] extends [void] ? () => void : (payload: P) => void. Та же форма у хука продьюсера событий.
Производные типы, которые можно именовать
Заголовок раздела «Производные типы, которые можно именовать»Их редко приходится называть — inline-generic на createTrigger всё прокидывает сам. Но иногда пишешь generic-обёртку для компонента или тест-хелпер, и тогда они важны:
EventOf<S> — discriminated union из пар (name, payload). Внутри обработчика свич по event.name автоматически сужает event.payload:
Цепочка типа ctx обработчика
Заголовок раздела «Цепочка типа ctx обработчика»Обработчик получает TriggerCtx<S, R>, где R — объединение обязательных условий. Шесть полей, каждое выведено из схемы или из R:
ConditionsCtx делает ключи R обязательными, остальные — опциональными:
ActionsCtx делает каждый ключ действия опциональным (реактор может быть не смонтирован) и добавляет цепочку модификаторов:
Это и есть type-system-причина, по которой каждый вызов действия в обработчике выглядит как actions.foo?.(payload) — ? покрывает случай «реактор пока не зарегистрирован». Страница Strict mode объясняет, почему это намеренно и как читать это без раздражения.
Переиспользование схемы — поделись один раз, уточняй пер-триггер
Заголовок раздела «Переиспользование схемы — поделись один раз, уточняй пер-триггер»В реальном приложении несколько триггеров ссылаются на одни и те же доменные типы. Вынеси их один раз:
Тогда схема каждого триггера — на один экран и очевидна:
Триггерам не обязательно делить схемы — одинаковые доменные типы и разные поверхности портов — это нормальный случай.
Брендированные id — защита кросс-API-вызовов
Заголовок раздела «Брендированные id — защита кросс-API-вызовов»Распространённый класс багов со строковыми id — передать customerId туда, где ждут channelId. TypeScript не отличает string от string. Брендированные типы — однострочное лекарство:
Использование в схеме:
Рантайм видит обычные строки (бренд — это phantom-поле, исчезающее на этапе компиляции). Единственная цена — одна функция-конструктор на каждый брендированный тип и одно место, куда положить валидацию, если она нужна.
Глубина generic — почему мы используем интерфейсы вместо глубоких generic
Заголовок раздела «Глубина generic — почему мы используем интерфейсы вместо глубоких generic»Публичные типы Triggery намеренно — shallow-generic + mapped-типы, а не вложенные conditional-лестницы. Причина: TypeScript-овский лимит глубины (Type instantiation is excessively deep …) на некоторых схемах включается уже на ~50 уровнях, а глубоко зацепленные infer-паттерны заметно подтормаживают IDE даже когда работают.
В своём коде:
- Не прокидывай схему через три слоя generic-обёрток. Каждый слой умножает стоимость.
- Предпочитай
type-алиасы computed property mapped-типам, когда нужно лишь назвать кусочек схемы. - Используй производные типы (
EventKey<S>,ActionMap<S>) вместо переоткрытия их через свои conditional.
Полезный smell-test: если редактору нужно больше полусекунды, чтобы показать hover-info на теле обработчика, — ты перешёл черту. Лечение почти всегда — ввести именованный промежуточный алиас.
Советы для очень больших схем
Заголовок раздела «Советы для очень больших схем»Когда у одного сценария легитимно 8+ событий и десяток действий, литерал схемы становится трудночитаемым. Помогают два паттерна.
Разделение по ролям
Заголовок раздела «Разделение по ролям»Бонус: схема становится переиспользуемой для тестов, где нужно типизировать stub-ctx.
Разделение самого триггера
Заголовок раздела «Разделение самого триггера»Если у одного триггера 4 события, делающие несвязанные вещи — это не один сценарий, это четыре. Разрежь на четыре файла .trigger.ts. Лимит размера неформальный, но реальный: большинство сценариев укладываются в 30–80 строк файла триггера вместе с импортами.
Solid и Vue: та же схема, те же типы
Заголовок раздела «Solid и Vue: та же схема, те же типы»Биндинги реэкспортируют те же TriggerSchema, EventKey, Trigger, TriggerCtx и т.д. из @triggery/core. Схема, которую ты пишешь для React-приложения, идентична для Solid и Vue — отличается только реализация хуков на фреймворк. Кроссфреймворковые кодовые базы получают ровно одно место для поиска имён портов.