将枚举值映射到类型
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 真的想在每个 :
之后缩进。)
我很想编写一个的对象,因此我可以使用T
和CallbackFor[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 => 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<T>
,这并不是真正不言自明的。
Try it out on TypeScript Playground,如果您设置了noImplicitAny
标志,请不要忘记提供registerCallback()
的返回类型。
【讨论】:
以上是关于将枚举值映射到类型的主要内容,如果未能解决你的问题,请参考以下文章
C++中如何将一个字符串(string类型的)映射(转换)到枚举值(enum)