将枚举值映射到类型

时间:2018-11-06 13:50:43

标签: typescript types enums dependent-type conditional-types

问题

假设我有一些像这样的代码:

// 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的类型系统的全部功能,也许还有更好的方法将枚举值映射到这样的类型。

问题

是否有更好的方法将枚举值映射到这样的类型?是否可以避免上述很大的条件类型? (实际上,我有很多事件,而且有点混乱:当我将鼠标悬停在CallbackFor上时,VS Code表现出了巨大的表情,而我的短毛呢真的想在每一个:之后缩进。)

我很想编写一个将枚举值映射到类型的对象,因此我可以使用registerCallbackT来声明CallbackFor[T],但这似乎不是问题。感谢您提供任何见解!

2 个答案:

答案 0 :(得分: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

如果只有三种事件类型,那么多个重载实际上是一个很好的解决方案:

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 any ? (t: T, f: EventTypeCallbackMap[T]) => void :
    never: never
> 


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

有关UnionToIntersection的说明,请参见here(并投票赞成答案)

答案 1 :(得分:1)

考虑使用覆盖声明,而不是设置复杂的映射:

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>类型,这并不是很容易解释。

Try it out on TypeScript Playground,并且如果设置了registerCallback()标志,请不要忘记为noImplicitAny提供返回类型。