does anyone know why in the second case the infer does not display the desired type?
type Emmit<C extends Controller<any, any>> = C extends Controller<infer T, any> ? T : never type On<C extends Controller<any, any>> = C extends Controller<infer E, infer O> ? O : never type E = BaseEvent<"a", 1> | BaseEvent<"b", 2> type O = BaseEvent<"c", 3> | BaseEvent<"d", 4> class A extends Controller<E, O> { } type a = Emmit<A> // BaseEvent<"a", 1> | BaseEvent<"b", 2>; type b = On<A>; // BaseEvent<string, any>
Advertisement
Answer
Cause
Extending this answer, the type inference for generics in class is done
- based on properties of the class
- based on type inference from methods.
The type of the first generic is inferred correctly inferred from the parameter of the method emit
which will be BaseEvent<"a", 1> | BaseEvent<"b", 2>
in case of class A
.
But for the second generic, OnEvent
is used in on
method only, which is again a generic and will not be inferred until on
is called. So the TS is not able to infer the correct type. It is only inferring the constraining type ie BaseEvent
or BaseEvent<string, any>
.
Even if you change on
method to-
on( event: EventType<OnEvent>, listener: OnListener<EventPayloadByType<EventType<OnEvent>, OnEvent>> ): void { this.emitter.on(event, listener); }
it will not infer correctly, as the type information OnEvent
is not stored as it is but with computed types using EventType
and OnListener
which is, I think, out of the capability of TS as of now.
Possible solution
Best solution that I can think of is adding a dummy property like private _dummy!: OnEvent
declare class EventEmitter { emit(t: string, p: any): void on(e: string, f: Function): void } export interface BaseEvent<Type extends string = string, Payload = any> { typ: Type, payload: Payload } export type EventType<Event extends BaseEvent> = Event extends BaseEvent<infer Type> ? Type : string export type EventPayloadByType< Type extends string, Event extends BaseEvent > = Event extends BaseEvent<Type, infer Payload> ? Payload : never export type OnListener<Payload = any> = (payload: Payload) => void; export class Emitter<EmmitEvent extends BaseEvent, OnEvent extends BaseEvent> { private readonly emitter = new EventEmitter(); private _dummy!: OnEvent // ^^^^^^ dummy property added here which stores type info for `OnEvent` emit(event: EmmitEvent): void { this.emitter.emit(event.typ, event.payload); } on<Event extends EmmitEvent | OnEvent, T extends EventType<Event>>( event: T, listener: OnListener<EventPayloadByType<T, Event>> ): void { this.emitter.on(event, listener); } } export abstract class Controller< EmmitEvent extends BaseEvent, OnEvent extends BaseEvent > extends Emitter<EmmitEvent, OnEvent> { } type Emmit<C extends Controller<any, any>> = C extends Controller<infer T, any> ? T : never type On<C extends Controller<any, any>> = C extends Controller<any, infer O> ? O : never type E = BaseEvent<"a", 1> | BaseEvent<"b", 2> type O = BaseEvent<"c", 3> | BaseEvent<"d", 4> class A extends Controller<E, O> { } type a = Emmit<A> // BaseEvent<"a", 1> | BaseEvent<"b", 2>; type b = On<A>; // BaseEvent<"c", 3> | BaseEvent<"d", 4> // ^^^^ correctly inferred now declare const e: A // type of the argument in emit is `E` ie. BaseEvent<"a", 1> | BaseEvent<"b", 2> e.emit({typ: "a", payload: 1})
NOTE– I have changed some names in the original code
BaseEvent.type
toBaseEvent.typ
(type
is a keyword in TS, can cause bugs/errors)EmmitEvents
toEmmitEvent
(it is a type for a single event)OnEvents
toOnEvent