Pot să refolosesc definiția parametrului unei funcții în Typescript?


Aș dori să capturez structura parametrilor la timpul de compilare a unei funcții pe care să o pot reutiliza în mai multe definiții de funcții cu semnături similare. Cred că acest lucru ar putea fi pe linia acestei probleme TS sau poate mai specific aceasta, dar nu sunt sigur că cazul meu de utilizare se aliniază neapărat cu acele propuneri, așa că ar putea fi posibil să fac acest lucru în Typescript-ul actual. Ceea ce încerc să fac este următorul lucru:

type FooArgs = ?{x: string, y: number}?; // syntax?
class MyEmitter extends EventEmitter {
  on(event: "foo", (...args: ...FooArgs) => void): this;
  emit(event: "foo", ...args: ...FooArgs): boolean;
}

Aceasta s-ar putea să nu fie cea mai inteligentă metodă de a face asta. Aș fi, de asemenea, bucuros să aflu despre o altă modalitate de a „copia” lista de argumente a unei metode către alta:

class MyEmitter extends EventEmitter {
  emit(event: "foo", x: string, y: number): boolean;
  on(event: "foo", (argumentsof(MyEmitter.emit)) => void): this;
}

dar nu cred că există un astfel de cuvânt cheie/funcție încorporată.

În treacăt, am încercat o abordare similară cu exemplele timpurii din acest articol, dar chiar și cu toate operațiile complexe de tip descrise mai târziu, acea abordare permite doar evenimente care emit zero sau un argument. Sper că, pentru acest caz de utilizare limitat, ar putea exista o metodă mai inteligentă.

Răspuns #1

Poți defini o interfață suplimentară care va conține toate definițiile funcțiilor. Și folosește tipuri mapate pentru a transforma aceste funcții în semnătura pentru on și semnătura pentru emit. Procesul este puțin diferit pentru cele două, așa că voi explica.

Să considerăm următoarea interfață de semnătură a evenimentului:

interface Events {
    scroll: (pos: Position, offset: Position) => void,
    mouseMove: (pos: Position) => void,
    mouseOther: (pos: string) => void,
    done: () => void
}

Pentru on vrem să creăm funcții noi care să ia ca prim argument numele proprietății din interfață și al doilea argument funcția însăși. Pentru a face acest lucru, putem folosi un tip mapat

type OnSignatures<T> = { [P in keyof T] : (event: P, listener: T[P])=> void }

Pentru emit, trebuie să adăugăm un parametru fiecărei funcții care să fie numele evenimentului, și putem folosi abordarea din acest răspuns

type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;
type AddParameters<T, P> =
    T extends (a: infer A, b: infer B, c: infer C) => infer R ? (
        IsValidArg<C> extends true ? (event: P, a: A, b: B, c: C) => R :
        IsValidArg<B> extends true ? (event: P, a: A, b: B) => R :
        IsValidArg<A> extends true ? (event: P, a: A) => R :
        (event: P) => R
    ) : never;

type EmitSignatures<T> = { [P in keyof T] : AddParameters<T[P], P>};

Acum că am transformat interfața originală, trebuie să combinăm toate funcțiile într-una singură. Pentru a obține toate semnăturile, am putea folosi T[keyof T] (adică EmitSignatures<Events>[keyof Events]), dar acest lucru ar returna o reuniune a tuturor semnăturilor și aceasta nu ar fi apelabilă. Aici intervine un tip interesant din acest răspuns sub forma UnionToIntersection, care va transforma reuniunea noastră de semnături într-o intersecție a tuturor semnăturilor.

Punând totul împreună obținem:

interface Events {
    scroll: (pos: Position, offset: Position) => void,
    mouseMove: (pos: Position) => void,
    mouseOther: (pos: string) => void,
    done: () => void
}
type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type OnSignatures<T> = { [P in keyof T]: (event: P, listener: T[P]) => void }
type OnAll<T> = UnionToIntersection<OnSignatures<T>[keyof T]>

type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;
    // Works for up to 3 parameters, but you could add more as needed
type AddParameters<T, P> =
    T extends (a: infer A, b: infer B, c: infer C) => infer R ? (
        IsValidArg<C> extends true ? (event: P, a: A, b: B, c: C) => R :
        IsValidArg<B> extends true ? (event: P, a: A, b: B) => R :
        IsValidArg<A> extends true ? (event: P, a: A) => R :
        (event: P) => R
    ) : never;

type EmitSignatures<T> = { [P in keyof T]: AddParameters<T[P], P> };

type EmitAll<T> = UnionToIntersection<EmitSignatures<T>[keyof T]>

interface TypedEventEmitter<T> {
    on: OnAll<T>
    emit: EmitAll<T>
}

declare const myEventEmitter: TypedEventEmitter<Events>;

myEventEmitter.on('mouseMove', pos => { }); // pos is position 
myEventEmitter.on('mouseOther', pos => { }); // pos is string
myEventEmitter.on('done', function () { });

myEventEmitter.emit('mouseMove', new Position());

myEventEmitter.emit('done');

Mulțumiri speciale lui @jcalz pentru o bucată din puzzle

Editare Dacă avem deja o clasă de bază care are implementări foarte generale ale on și emit, trebuie să facem puțină îndoire a cotului cu sistemul de tipuri.

// Base class
class EventEmitter {
    on(event: string | symbol, listener: (...args: any[]) => void): this { return this;}
    emit(event: string | symbol, ...args: any[]): this { return this;}
}

interface ITypedEventEmitter<T> {
    on: OnAll<T>
    emit: EmitAll<T>
}
// Optional derived class if we need it (if we have nothing to add we can just us EventEmitter directly 
class TypedEventEmitterImpl extends EventEmitter  {
}
// Define the actual constructor, we need to use a type assertion to make the `EventEmitter` fit  in here 
const TypedEventEmitter : { new <T>() : TypedEventEmitter<T> } =  TypedEventEmitterImpl as any;
// Define the type for our emitter 
type TypedEventEmitter<T> =  ITypedEventEmitter<T> & EventEmitter // Order matters here, we want our overloads to be considered first

// We can now build the class and use it as before
const myEventEmitter: TypedEventEmitter<Events> = new TypedEventEmitter<Events>();

Editare pentru 3.0

De la momentul scrierii, TypeScript și-a îmbunătățit capacitatea de a mapa funcții. Cu tupluri în parametrii rest și expresii de răspândire, putem înlocui multiplele supraîncărcări ale AddParameters cu o versiune mai curată (și IsValidArg nu mai este necesar):

type AddParameters<T, P> =
    T extends (...a: infer A) => infer R ? (event: P, ...a: A) => R : never;