Skip to content

Infer does not work in generics as second param

does anyone know why in the second case the infer does not display the desired type?

playground

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>

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})

Playground

NOTE– I have changed some names in the original code

  • BaseEvent.type to BaseEvent.typ (type is a keyword in TS, can cause bugs/errors)
  • EmmitEvents to EmmitEvent (it is a type for a single event)
  • OnEvents to OnEvent