Выведение типов аргументов дженерик метода, в теле дочернего класса

Сложно сформулировать четкий вопрос. Проще показать на примере: Ссылка на TS Playground

Хочу сделать типизированный EventEmitter, с возможностью наследования и расширения списка типизированных событий. Но typescript вставляет "палки в колеса".

Если среди вас есть гуру typescript, пожалуйста, помогите!

// базовый тип списка событий
type TEventsList = Record<string, (...args: [any]) => void>;

// есть типизированный класс EventEmitter
declare class TypedEventEmitter<TEvents extends TEventsList> {
    addListener<E extends keyof TEvents>(event: E, listener: TEvents[E]): this;
    on<E extends keyof TEvents>(event: E, listener: TEvents[E]): this;
    once<E extends keyof TEvents>(event: E, listener: TEvents[E]): this;
    removeListener<E extends keyof TEvents>(event: E, listener: TEvents[E]): this;
    off<E extends keyof TEvents>(event: E, listener: TEvents[E]): this;
    removeAllListeners<E extends keyof TEvents>(event: E): this;
    setMaxListeners(n: number): this;
    getMaxListeners(): number;
    protectedlisteners<E extends keyof TEvents>(event: E): TEvents[E][];
    rawListeners<E extends keyof TEvents>(event: E): TEvents[E][];
    listenerCount<E extends keyof TEvents>(type: E): number;
    prependListener<E extends keyof TEvents>(event: E, listener: TEvents[E]): this;
    prependOnceListener<E extends keyof TEvents>(event: E, listener: TEvents[E]): this;
    eventNames<E extends keyof TEvents>(): Array<E>;

    // проблема с этим
    emit<E extends keyof TEvents, A extends Parameters<TEvents[E]>>(event: E, ...args: A): boolean;

    // единственный вариант сделать так, но тогда не будет работать проверка типов аргументов вызываемых событий
    // emit<E extends keyof TEvents>(event: E, ...args: any[]): boolean;
}

// события которые есть у BaseClass 
type TBaseClassEvents = {
    event1(): void;
    event2(arg1: string): void;
    event3(arg1: number, arg2: boolean): void;
}

// есть некий класс, у которого есть свои события и от которого можно унаследоваться, добавив ему дополнительные события
class BaseClass<TAdditionalEvents extends TEventsList> extends TypedEventEmitter<TAdditionalEvents & TBaseClassEvents> {
    testBaseClassMethod() {
        // проблема тут, тайпскрипт ругается
        this.emit("event2", "Hello from BaseClass"); // тут ошибка
    }
}

const baseClassInstance = new BaseClass();
baseClassInstance.emit("event2", "Hello from BaseClass"); // тут уже нет ошибки


// некий дочерний класс унаследованный от BaseClass
class ChildClass extends BaseClass<{ event4(arg1: boolean): void }> {
    testChildClassMethod() {
        // тут все прекрасно работает
        this.emit("event1");
        this.emit("event2", "Hello from ChildClass");
        this.emit("event4", true);
    }
}

const childClassInstance = new ChildClass();
childClassInstance.emit("event2", "Hello from BaseClass"); // и тут нет ошибки

Ответы (1 шт):

Автор решения: qwabra

1

// некий дочерний класс унаследованный от BaseClass
class ChildClass extends BaseClass<{ event4(arg1: boolean): void }>

"BaseClass" это TypedEventEmitter !

песочница

// -,-
// -,-
type TEventsList = Record<string, (...args: any[]) => void>
// -,-
declare class TypedEventEmitter<TEvents extends TEventsList> {
    addListener<E extends keyof TEvents>(event: E, listener: TEvents[E]): this
    on<E extends keyof TEvents>(event: E, listener: TEvents[E]): this
    once<E extends keyof TEvents>(event: E, listener: TEvents[E]): this
    removeListener<E extends keyof TEvents>(event: E, listener: TEvents[E]): this
    off<E extends keyof TEvents>(event: E, listener: TEvents[E]): this
    removeAllListeners<E extends keyof TEvents>(event: E): this
    setMaxListeners(n: number): this
    getMaxListeners(): number
    protectedlisteners<E extends keyof TEvents>(event: E): TEvents[E][]
    rawListeners<E extends keyof TEvents>(event: E): TEvents[E][]
    listenerCount<E extends keyof TEvents>(type: E): number
    prependListener<E extends keyof TEvents>(event: E, listener: TEvents[E]): this
    prependOnceListener<E extends keyof TEvents>(event: E, listener: TEvents[E]): this
    eventNames<E extends keyof TEvents>(): Array<E>

    // проблема с этим
    emit<E extends keyof TEvents, A extends Parameters<TEvents[E]>>(event: E, ...args: A): boolean
}
// -, -
// -,-
// - ,-
namespace ClassA {
    export interface TEvents extends TEventsList {
        event1(): void
        event2(arg1: string): void
        event3(arg1: number, arg2: boolean): void
    }
}
class ClassA extends TypedEventEmitter<ClassA.TEvents> {
    testClassAMethod() {
        this.emit("event2", "Hello from ClassA")
        this.emit("event1")
    }
}
{//test
    const instance = new ClassA
    // -,-
    instance.emit("event2", 'str')
    // @ts-expect-error
    instance.emit("event2", 123)
    // @ts-expect-error
    instance.emit("event2")
}
// -, -
// -,-
// - ,-
namespace ClassB {
    export type TEvents = ClassA.TEvents
        & { event4(arg1: boolean): void }
        & { event8(arg1?: boolean): void }
}
class ClassB extends TypedEventEmitter<ClassB.TEvents> {
    testClassBMethod() {
        this.emit("event1")
        this.emit("event2", "Hello from ClassB")
        this.emit("event4", true)
    }
}
// -,-
{//test
    const instance = new ClassB
    // -,-
    {
        instance.emit("event4", true)
        // @ts-expect-error
        instance.emit("event4", 123)
        // @ts-expect-error
        instance.emit("event4")
    }
    // -,-
    {
        instance.emit("event8")
    }
}
// -, -

UPD: fix autocomplete

type TEventsList = Record<string, (...args: any[]) => void>
declare class TypedEventEmitter<TEvents extends TEventsList> {
    emit<E extends keyof TEvents, A extends Parameters<TEvents[E]>>(event: E, ...args: A): boolean
}
// -,-
namespace ClassA {
    // export interface TEvents extends TEventsList {
    //     event1(): void
    //     event2(arg1: string): void
    //     event3(arg1: number, arg2: boolean): void
    // }
    // TODO: fix autocomplete
    export type TEvents = {
        event1(): void
        event2(arg1: string): void
        event3(arg1: number, arg2: boolean): void
    }
}
class ClassA extends TypedEventEmitter<ClassA.TEvents> { }
{//test
    const instance = new ClassA
    instance.emit('event1')
}

EventTarget

EventTarget - https://developer.mozilla.org/ru/docs/Web/API/EventTarget

class Q extends EventTarget { }

{//test
    const q = new Q
    q.addEventListener('qwa', () => { })
    const qwa = new Event('qwa')
    q.dispatchEvent(qwa)
}
→ Ссылка