将枚举值映射到类型

Posted

技术标签:

【中文标题】将枚举值映射到类型【英文标题】:Mapping enum values to types 【发布时间】:2019-04-09 22:15:36 【问题描述】:

问题

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

// 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 会显示一个巨大的表情,而我的 linter 真的想在每个 : 之后缩进。)

我很想编写一个的对象,因此我可以使用TCallbackFor[T] 声明registerCallback,但这似乎不是一回事。任何见解都值得赞赏!

【问题讨论】:

【参考方案1】:

我们可以创建一个在枚举成员和回调类型之间映射的类型,但是如果我们直接在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

请参阅here(并为答案投票)以了解UnionToIntersection的解释

【讨论】:

不错的答案! UnionToIntersection 类型看起来很神奇。你能解释一下它在这里取得了什么成就吗? @PatrickRoberts 使用条件类型的分布属性,它们分布在裸类型参数上,这就是为什么复杂的EventType extends infer T ? T extends any ? 我们引入一个类型参数并分布在它上面。 UnionToIntersection 中的 U 相同 @Lynn 如果您启用 noImplictAny,您将在 n 上收到错误,因为它的类型为 any 而不是您所期望的 string。 Typescript 通常无法做到这一点,如果回调类型本身依赖于另一个类型参数,则推断回调参数。最后一种方法有效,因为我们基本上预先生成了所有可能的签名来模拟多个重载,并且回调的类型不取决于第一个参数的类型,而是取决于所选的重载(这确实取决于第一个参数.. . 是的,打字稿有时很奇怪 :) ) @Lynn yes 指定错误的参数会出错,但不指定任何内容会让你得到n:any,这意味着你可以做registerCallback(EventType.PlaySong, n =&gt; n.ddd ) // 首先,这太棒了。谢谢你。其次,在较新版本的 Typescript 中,我不得不将 T extends any ? 更改为 T extends EventType ?。否则,EventTypeCallbackMap[T] 会出现错误:“Type 'T' cannot be used to index type 'EventTypeCallbackMap'.(2536)”【参考方案2】:

与其设置复杂的映射,不如考虑使用覆盖声明:

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&lt;T&gt;,这并不是真正不言自明的。

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

【讨论】:

以上是关于将枚举值映射到类型的主要内容,如果未能解决你的问题,请参考以下文章

C++中如何将一个字符串(string类型的)映射(转换)到枚举值(enum)

Grails:将枚举类型的mysql字段映射到域类

如何使用 JPA 和 Hibernate 映射 PostgreSQL 枚举

将枚举映射到键或值的类型

JavaSE--枚举类

将 graphql 枚举映射到 kotlin 枚举