Mapping enum values to types

混江龙づ霸主 提交于 2020-06-28 05:36:15

问题


The problem

Suppose I have some code like this:

// 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;

In the API I'm given, I can register such a callback with

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

But I want to get rid of that any[] and make sure I can't register an ill-typed callback function.

A solution?

I realized I can do this:

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) => { /* ... */ })

This is really nifty and powerful! It feels like I'm using dependent types: I basically wrote myself a function mapping values to types, here.

However, I don't know the full strength of TypeScript's type system and maybe there is an even better way to map enum values to types like this.

The question

Is there a better way to map enum values to types like this? Can I avoid a really big conditional type as above? (In reality I have many events, and it's kind of a mess: VS Code shows a huge expression when I hover over CallbackFor, and my linter really wants to indent after every :.)

I'd love to write an object mapping enum values to types, so I can declare registerCallback using T and CallbackFor[T], but that doesn't seem to be a thing. Any insights are appreciated!


回答1:


We can create a type that maps between the enum members and the callback types, but the if we use it directly in registerCallback we will not get correct inference for callback argument types:

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

If you have just 3 event types, multiple overloads are actually a pretty good solution:

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

If you have a lot of enum member you could also generate the overload signature automatically:

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

See here (and up-vote the answer) for an explanation of UnionToIntersection




回答2:


Instead of setting up a complicated mapping, consider using override declarations:

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

I find this much more readable and maintainable than setting up an explicit mapping type, though I understand the inconvenience of not having a single generic signature. One thing you have to remember is that people using your API will definitely prefer the transparency of override declarations to the opaque type CallbackFor<T> which isn't really self-explanatory.

Try it out on TypeScript Playground, and don't forget to provide the return type for registerCallback() if you have the noImplicitAny flag set.



来源:https://stackoverflow.com/questions/53173280/mapping-enum-values-to-types

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!