Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
495 views
in Technique[技术] by (71.8m points)

Can I reuse the parameter definition of a function in Typescript?

I would like to capture the compile-time parameter structure of a function that I can reuse in multiple function definitions with similar signatures. I think this might be along the lines of this TS issue or maybe more specifically this one, but I'm not sure my use case necessarily lines up with those proposals, so it might be possible to do this in current Typescript. What I'm trying to do is this:

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

This might not be the smartest way to do it. I would also be happy to learn about some other way to "copy" one method's argument list to another:

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

but I don't believe any such keyword / builtin exists.

As an aside, I have tried an approach similar to the early examples in this article but even with all the complex type operations described later, that approach only allows for events that emit zero or one arguments. I'm hoping that, for this limited use case, there might be a smarter way.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

You can define an extra interface that will contain all the function definitions. And use mapped types to transform these functions into the signature for on and the signature for emit. The process is a bit different for the two so I will explain.

Lets consider the following event signature interface:

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

For on we want to create new functions that take as first argument the name of the property in the interface and the second argument the function itself. To do this we can use a mapped type

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

For emit, we need to add a parameter to each function that is the event name, and we can use the approach in this answer

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

Now that we have the original interface transformed we need to smush all the functions into a single one. To get all the signatures we could use T[keyof T] (ie EmitSignatures<Events>[keyof Events]) but this would return a union of all the signatures and this would not be callable. This is where an interesting type comes in from this answer in the form of UnionToIntersection which will transform our union of signatures into an intersection of all signatures.

Putting it all together we get:

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');

Special thanks to @jcalz for a piece of the puzzle


Edit If we already have a base class that has very general implementations of on and emit we need to do a bit of elbow twisting with the type system.

// 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>();

Edit for 3.0

Since the time of writing, typescript has improved it's ability to map functions. With Tuples in rest parameters and spread expressions we can replace the multiple overloads of AddParameters with a cleaner version (and IsValidArg is not required):

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...