Я создаю пакет, который позволяет вам определять «действия», а затем «запускать» их. Действия — это функции, которые принимают некоторую полезную нагрузку и обратный вызов для рекурсивного запуска дополнительных действий.
В идеале использование пакета должно выглядеть так:
import { ActionDef, getTrigger } from 'thePackage';
const action1 = ((payload, trigger) => {
console.info('action1', payload)
trigger('action2', {y: 'abc'})
}) satisfies ActionDef<MyActions, {x: number}>;
const action2 = ((payload, trigger) => {
console.info('action2', payload)
trigger('action1', {x: 123})
}) satisfies ActionDef<MyActions, {y: string}>;
const actions = {
action1,
action2
}
type MyActions = typeof actions;
const trigger = getTrigger(actions);
function triggerSomeActions() {
trigger('action1', {x: 123});
trigger('action1', {x: 234});
trigger('action2', {y: 'abc'});
}
Я хочу, чтобы машинописный текст проверял, что вызовы trigger (как из getTrigger, так и в виде обратных вызовов) действительны (действительное имя и действительная соответствующая полезная нагрузка).
Теперь это не работает из-за ссылки на циклический тип MyActions.
Мне интересно, есть ли способ правильно напечатать это, не слишком раздражая разработчика, использующего пакет.
На данный момент в упаковке у меня есть следующие типы:
type ActionDef<A extends Actions, P> = (payload: P, triggerCallback: TriggerFunc<A>) => void
type Actions = Record<string, ActionDef<any, any>>;
type PayloadFromDef<D> = D extends ActionDef<any, infer P> ? P : never
type TriggerFunc<A extends Actions> = (<K extends keyof A>(actionName: K, payload: PayloadFromDef<A[K]>) => void);
declare function getTrigger<A extends Actions>(actions: A): TriggerFunc<A>;
TypeScript Playground со всем этим
Одним из решений, позволяющих избавиться от ссылки на циклический тип, было бы отдельное перечисление имен действий и типов полезных данных, например:
type ActionTypes = {
action1: {x: number};
action2: {y: string};
}
а затем используйте это вместо MyActions при определении функций действия, но это повредит DevX.
(см. предыдущий комментарий) Итак, эта ссылка на игровую площадку показывает то, что я бы назвал минимально воспроизводимым примером вашей ситуации, за исключением того, что я действительно не могу понять, почему это еще не так, как вам хотелось бы. продолжать. Ваш объект actions по своей сути является круглым, и, безусловно, самый простой подход — просто присвоить ему тип для начала. Не могли бы вы продемонстрировать, почему опыт разработки здесь хуже, чем любые препятствия, через которые можно пройти, чтобы заставить TS вывести типы?
Большое спасибо @jcalz! Меня раздражает DevX, если вы определяете эти действия отдельно: в реальном сценарии эти функции действий на самом деле являются объектами, содержащими множество сложных функций, и определение их всех в одном большом «ActionMap» не является идеальным. При определении их отдельно вы получаете имя каждого действия в 3 местах вместо 1 в идеальном сценарии (например, тот, которого я пытаюсь достичь с помощью версии с неработающей циклической зависимостью). Вот ссылка на площадку, где я определяю действия отдельно.
• Не могли бы вы отредактировать , чтобы сделать минимально воспроизводимый пример маленьким, как тот, о котором мы говорим в ссылках на игровую площадку? • Опять же, я не понимаю, как можно было бы сделать это намного лучше, чем определить интерфейс и затем ссылаться на него, что для меня звучит как 2 места (или 3, если вам нужно разбить каждую вещь на несколько частей, а затем объединить их). вместе). Ваша структура данных по своей сути является циклической. У class есть некоторая поддержка такой цикличности, но не похоже, что это здесь поможет. Честно говоря, я думаю, что ответ здесь: «Нет, вы не можете сделать ничего лучше».
(см. предыдущий комментарий, особенно часть о редактировании ). Итак, самое близкое, что я могу получить к чему-то подобному, — это использовать объявление class , как показано в этой ссылке на игровую площадку . Теперь вы можете назвать каждый тип полезной нагрузки ровно один раз. Лично я считаю, что это хуже, чем просто заранее определить интерфейс. В любом случае, это полностью решает вопрос? Если да, то я мог бы написать ответ, иначе чего мне не хватает в этом вопросе?
Я упростил вопрос. Спасибо за class версию. Я согласен с вами, что предыдущая версия (с определением интерфейса) кажется лучшим, что мы можем здесь сделать!
У меня действительно возникают проблемы при попытке использовать вашу упрощенную версию, поскольку вы ограничиваете вещи Actions, но по ходу дела кажется, что полезные нагрузки сами по себе являются функциями. Не могли бы вы четырежды проверить этот код, а затем либо сказать мне, почему вы пишете его таким образом, либо отредактировать так, чтобы это не делало полезную нагрузку функций? В идеале вы должны создать минимальный воспроизводимый пример, который показывает проблему (с цикличностью), решение, которое вам не нравится (которое работает), а затем мы можем ответить: «Это решение — лучший способ, все остальное — худший".
Вы правы, там должно было быть немного MyActions. В любом случае я чувствую, что вы уже более чем ответили на мой вопрос. Если вы приложите в ответ примеры своих игровых площадок, я с радостью приму это.






Тип actions по своей сути является циклическим, и просить TypeScript определить такой тип для вас не будет работать очень хорошо. Хотя объявления классов обычно позволяют выводить типы одного свойства на основе выведенных типов других свойств, произвольные объектные литералы и функции обычно не работают таким образом.
Было бы гораздо проще определить простой интерфейс сопоставления имени действия с типом полезной нагрузки (например, ваш ActionTypes), а затем реализовать объект действия этого типа. Вы говорите, что это не идеальный опыт разработчика, но это лучше, чем пытаться перепрыгнуть через обручи, избегающие цикличности. В вашем примере не показано, как это выглядит, поэтому я бы сказал, что это наиболее разумная реализация.
Сначала мы меняем ваши типы на:
type ActionDef<AP, K extends keyof AP> =
(payload: AP[K], triggerCallback: TriggerFunc<AP>) => void
type TriggerFunc<AP> =
<K extends keyof AP>(actionName: K, payload: AP[K]) => void;
type ActionMap<AP> = { [K in keyof AP]: ActionDef<AP, K> }
declare function getTrigger<AP>(actions: ActionMap<AP>): TriggerFunc<AP>;
На самом деле я ничего не пишу с точки зрения типа действий (A); это просто типы полезных данных (AP) и ключей (K). Обратите внимание, что вам больше не нужно ограничивать что-либо типом действия, а также вам не нужно явно выводить что-либо при описании типов. И ActionDef нужно знать только ключ K из AP, соответствующий полезной нагрузке; он может вычислить полезную нагрузку как AP[K]. Наконец, функция getTrigger() выводит AP из сопоставленного типа ActionMap<AP> и возвращает TriggerFunc<AP>.
Хорошо, теперь для рабочего примера мы заранее определяем этот интерфейс отображения:
interface ActionTypes {
action1: { x: number };
action2: { y: string };
}
Тогда сборка actions происходит так же, как и раньше, за исключением того, что вы используете новые определения типов:
const action1 = ((payload, trigger) => {
console.info('action1', payload)
trigger('action2', { y: 'abc' })
}) satisfies ActionDef<ActionTypes, "action1">;
const action2 = ((payload, trigger) => {
console.info('action2', payload)
trigger('action1', { x: 123 })
}) satisfies ActionDef<ActionTypes, "action2">;
const actions = {
action1,
action2
}
const trigger = getTrigger(actions);
function triggerSomeActions() {
trigger('action1', { x: 123 });
trigger('action1', { x: 234 });
trigger('action2', { y: 'abc' });
}
Мне это действительно кажется не очень плохим. Фактически, большая часть дублирования возникает из-за создания отдельного именованного функционального объекта для каждого действия перед его упаковкой в actions. Определение их отдельно может быть важно для вас, но вы можете сократить избыточность, определив их напрямую:
const actions: ActionMap<ActionTypes> = {
action1: ((payload, trigger) => {
console.info('action1', payload)
trigger('action2', { y: 'abc' })
}),
action2: ((payload, trigger) => {
console.info('action2', payload)
trigger('action1', { x: 123 })
})
};
Это уже настолько кратко, что мне трудно представить что-то лучшее, предполагающее использование typeof или какого-либо другого косвенного обращения. Например, вы можете воспользоваться циклической обработкой объявлений class, создав класс ActionsClass, экземпляры которого вам нужны actions. Учитывая такой класс, вы можете определить соответствующий тип утилиты триггерной функции:
type ClassTrigger<T> =
TriggerFunc<{ [K in keyof T]: T[K] extends (payload: infer P, ...a: any) => void ? P : never }>
А затем вместе определите ActionsClass и тип триггера:
type ACT = ClassTrigger<ActionsClass>;
class ActionsClass {
action1(payload: { x: number }, trigger: ACT) {
console.info('action1', payload);
trigger('action2', { y: 'abc' });
};
action2(payload: { y: string }, trigger: ACT) {
console.info('action2', payload);
trigger('action1', { x: 123 })
}
}
const actions = new ActionsClass();
Это позволяет достичь цели: не нужно писать action1, action2, {x: number} или {y: string} более одного раза (конечно, игнорируя вызовы trigger()) и не полагаясь на внешнее сопоставление ActionTypes. Но это так странно, и я скорее попрошу разработчиков определить их типы, чем скажу им, почему они создают класс, когда им нужен только один его экземпляр.
Мне трудно представить, как вы можете использовать это
ActionTypesрешение для решения своей проблемы. Не могли бы вы отредактировать , чтобы показать, как это работает и как это вредит опыту разработчиков? Кроме того, есть ли способ сократить минимальный воспроизводимый пример до чего-то, требующего менее 60 строк кода? Как только нам нужно начать прокручивать, чтобы увидеть, что происходит, решить проблему становится сложнее.