简体   繁体   中英

Infer a mapped parameter type in typescript

I'm working with a library that has an interesting type declaration (simplified):

class Emitter {
    public on<EventName extends 'streamCreated' | 'streamDestroyed'> (eventName: EventName, callback: (event: {
        streamCreated: {id: number};
        streamDestroyed: {reason: string};
    }[EventName]) => void): void;
}

And I'm trying to type the event in the callback that I provide:

const callback = (event: StreamDestroyedEvent) => console.log(event.reason);
emitter.on('streamDestroyed', callback);

However "StreamDestroyedEvent" doesn't exist. It's not provided by the library, but rather exists only in that anonymous eventmapping, so I instead try to infer it:

type InferCallback<T, E> = T extends (eventName: E, event: infer R) => void ? R : never;
type InferCallbackEvent<T, E> = InferCallback<T, E> extends (event: infer P) => void ? P : never;
type StreamDestroyedEvent = InferCallbackEvent<Emitter['on'], 'streamDestroyed'>;

However, instead of giving me a type of {reason: string} it get the union type {reason: string;} | {id: number;} {reason: string;} | {id: number;} . How can I get the right type, or is this as close as I'm going to get?

You'd like to basically "plug in" a type for the EventName type parameter. While TypeScript supports this when calling a generic function, it doesn't do a good job representing such types in the type system itself. To do so would fully would require support for higher rank types , which aren't really part of the language. And the emitter.on function by itself doesn't really let you call it "partially" so that you plug in EventName with the first parameter and then have the compiler tell you what the type of the second parameter should be.

Before TypeScript 3.4 came out I'd probably say getting this information out of the compiler would be impossible. I'd try something like

emitter.on("streamDestroyed", x => {
  type StreamDestroyedEvent = typeof x; // can't get it out of the scope though!
})

and then be unable to get the StreamDestroyedEvent type to escape the function.

TypeScript 3.4 introduced improved support higher order type inference from generic functions . You still can't represent what you're doing purely in the type system, but now we can define a function that gives us something we can that does the "partial" calling and retains the type information we need.

Here is a currying function which takes a multi-argument function and returns a new function of one argument that returns another function of the remaining arguments:

const curry = <T, U extends any[], R>(
  cb: (t: T, ...args: U) => R
) => (t: T) => (...args: U) => cb(t, ...args);

If you call curry on emitter.on , and then call that with "streamDestroyed" , (in TS3.4+) you get a function which takes a callback which takes a StreamDestroyedEvent , which you can now capture:

const streamDestroyedFunc = curry(emitter.on.bind(emitter))("streamDestroyed")
type StreamDestroyedEvent = Parameters<Parameters<typeof streamDestroyedFunc>[0]>[0];
// type StreamDestroyedEvent = { reason: string; }

Note that the actual runtime behavior of the above isn't the point; I tried to make sure it would actually do something reasonable, but you could also have just used type assertions to lie to the compiler about what was happening, as long as you don't get a runtime error when you run it:

const fakeCurry: typeof curry = () => () => null!;
const fakeOn: Emitter["on"] = null!;
const fakeFunc = fakeCurry(fakeOn)("streamDestroyed");
type SDE2 = Parameters<Parameters<typeof fakeFunc>[0]>[0]; // same thing

Okay, hope that helps; good luck!

Link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM