Сопоставление значений перечисления с типами

Эта проблема

Предположим, у меня есть такой код:

// Events we might receive:
enum EventType { PlaySong, SeekTo, StopSong };

// Callbacks we would handle them with:
type PlaySongCallback = (name: string) => void;
type SeekToCallback = (seconds: number) => void;
type StopSongCallback = () => void;

В предоставленном мне API я могу зарегистрировать такой обратный вызов с помощью

declare function registerCallback(t: EventType, f: (...args: any[]) => void);

Но я хочу избавиться от этого any[] и убедиться, что я не могу зарегистрировать функцию обратного вызова с неправильным типом.

Решение?

Я понял, что могу сделать это:

type CallbackFor<T extends EventType> =
    T extends EventType.PlaySong
        ? PlaySongCallback
        : T extends EventType.SeekTo
            ? SeekToCallback
            : T extends EventType.StopSong
                ? StopSongCallback
                : never;

declare function registerCallback<T extends EventType>(t: T, f: CallbackFor<T>);

// Rendering this valid:
registerCallback(EventType.PlaySong, (name: string) => { /* ... */ })

// But these invalid:
// registerCallback(EventType.PlaySong, (x: boolean) => { /* ... */ })
// registerCallback(EventType.SeekTo, (name: string) => { /* ... */ })

Это действительно круто и мощно! Такое ощущение, что я использую зависимые типы: я в основном написал здесь функцию, отображающую значения в типы.

Однако я не знаю всей силы системы типов TypeScript, и, возможно, есть еще лучший способ сопоставить значения перечисления с типами, подобными этому.

Вопрос

Есть ли лучший способ сопоставить значения перечисления с типами, подобными этому? Можно ли избежать очень большого условного типа, как указано выше? (На самом деле у меня много событий, и это какой-то беспорядок: VS Code показывает огромное выражение, когда я навожу курсор на CallbackFor, а мой линтер действительно хочет отступать после каждого :.)

Я хотел бы написать объект, отображающий значения перечисления в типы, поэтому я могу объявить registerCallback, используя T и CallbackFor[T], но, похоже, это не так. Любые идеи приветствуются!


person Lynn    schedule 06.11.2018    source источник


Ответы (2)


Мы можем создать тип, который сопоставляется между элементами перечисления и типами обратного вызова, но если мы используем его непосредственно в registerCallback, мы не получим правильного вывода для типов аргументов обратного вызова:

type EventTypeCallbackMap = {
    [EventType.PlaySong] : PlaySongCallback,
    [EventType.SeekTo] : SeekToCallback,
    [EventType.StopSong] : StopSongCallback,
}

declare function registerCallback
    <T extends EventType>(t: T, f: EventTypeCallbackMap[T]): void;

registerCallback(EventType.PlaySong, n => { }) // n is any

Если у вас всего 3 типа событий, несколько перегрузок на самом деле являются довольно хорошим решением:

declare function registerCallback(t: EventType.PlaySong, f: PlaySongCallback): void;
declare function registerCallback(t: EventType.SeekTo, f: SeekToCallback): void;
declare function registerCallback(t: EventType.StopSong, f: StopSongCallback): void;

registerCallback(EventType.PlaySong, n => { }) // n is string

Если у вас много членов перечисления, вы также можете автоматически сгенерировать сигнатуру перегрузки:

type EventTypeCallbackMap = {
    [EventType.PlaySong]: PlaySongCallback,
    [EventType.SeekTo]: SeekToCallback,
    [EventType.StopSong]: StopSongCallback,
}

type UnionToIntersection<U> = 
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
declare let registerCallback: UnionToIntersection<
    EventType extends infer T ?
    T extends T ? (t: T, f: EventTypeCallbackMap[T]) => void :
    never: never
> 


registerCallback(EventType.PlaySong, n => { }) // n is string

См. здесь (и проголосуйте за ответ) для объяснения UnionToIntersection

person Titian Cernicova-Dragomir    schedule 06.11.2018
comment
Хороший ответ! Этот тип UnionToIntersection выглядит довольно волшебно. Не могли бы вы объяснить, чего он достигает здесь? - person Lynn; 06.11.2018
comment
Также действительно ли необходимо U extends any ? (k: U) => void : never? Почему это не может быть просто (k: U) => void? - person Patrick Roberts; 06.11.2018
comment
@Lynn добавила ссылку на пересечение unionto, которое принадлежит jcalz - person Titian Cernicova-Dragomir; 06.11.2018
comment
@PatrickRoberts Это использует распределительное свойство условных типов, они распределяются по параметрам открытого типа, поэтому запутанный EventType extends infer T ? T extends any ? мы вводим параметр типа и распределяем по нему. То же самое для U в UnionToIntersection - person Titian Cernicova-Dragomir; 06.11.2018
comment
@PatrickRoberts, вы можете узнать больше об этом typescriptlang.org/docs/handbook/advanced- types.html, если вы ищете Распространяемые условные типы - person Titian Cernicova-Dragomir; 06.11.2018
comment
Спасибо! Кстати, у вас есть идеи, почему вывод n терпит неудачу? Любопытно, но когда я пробую ваш первый пример на игровой площадке и навожу курсор на вызов registerCallback, я вижу функция registerCallback‹EventType.PlaySong›(t: EventType.PlaySong, f: PlaySongCallback) во всплывающей подсказке. Таким образом, кажется, что тип f выводится правильно, по крайней мере, и тогда я не понимаю, почему вывод n потерпит неудачу. - person Lynn; 06.11.2018
comment
@Lynn, если вы включите noImplictAny, вы получите сообщение об ошибке n, поскольку оно набирается как any, а не string, как вы ожидаете. Typescript обычно не может этого сделать, выведите аргументы обратного вызова, если сам тип обратного вызова зависит от другого параметра типа. Последний подход работает, потому что мы в основном предварительно генерируем все возможные сигнатуры, имитирующие множественные перегрузки, и там тип обратного вызова зависит не от типа первого аргумента, а скорее от выбранной перегрузки (которая действительно зависит от первого аргумента. .да машинопись иногда странная :) ) - person Titian Cernicova-Dragomir; 06.11.2018
comment
(На самом деле, передача (n: number) => { } приводит к ошибке!) - person Lynn; 06.11.2018
comment
@Lynn да, указание неправильного аргумента приведет к ошибке, но если ничего не указать, вы получите n:any, что означает, что вы можете сделать registerCallback(EventType.PlaySong, n => { n.ddd }) // - person Titian Cernicova-Dragomir; 06.11.2018
comment
Во-первых, это потрясающе. Спасибо. Во-вторых, в более поздних версиях Typescript мне приходилось менять T extends any ? на T extends EventType ?. В противном случае вы получите сообщение об ошибке EventTypeCallbackMap[T]: тип «T» нельзя использовать для индексации типа «EventTypeCallbackMap». (2536) - person Taytay; 28.05.2020

Вместо настройки сложного сопоставления рассмотрите возможность использования объявлений переопределения:

declare function registerCallback(t: EventType.PlaySong, f: PlaySongCallback);
declare function registerCallback(t: EventType.SeekTo, f: SeekToCallback);
declare function registerCallback(t: EventType.StopSong, f: StopSongCallback);

Я нахожу это гораздо более удобным для чтения и сопровождения, чем установка явного типа сопоставления, хотя я понимаю неудобство отсутствия единой универсальной подписи. Вы должны помнить одну вещь: люди, использующие ваш API, определенно предпочтут прозрачность объявлений переопределения непрозрачному типу CallbackFor<T>, который на самом деле не говорит сам за себя.

Попробуйте на TypeScript Playground, и не забудьте указать тип возвращаемого значения для registerCallback(), если у вас установлен флаг noImplicitAny.

person Patrick Roberts    schedule 06.11.2018