Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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.

like image 883
Coderer Avatar asked May 16 '18 11:05

Coderer


People also ask

How do you define a function with parameters in TypeScript?

Function Type Expressions The syntax (a: string) => void means “a function with one parameter, named a , of type string, that doesn't have a return value”. Just like with function declarations, if a parameter type isn't specified, it's implicitly any . Note that the parameter name is required.

Can you pass a function as a parameter in TypeScript?

Similar to JavaScript, to pass a function as a parameter in TypeScript, define a function expecting a parameter that will receive the callback function, then trigger the callback function inside the parent function.

How do you define a function in interface TypeScript?

TypeScript Interface can be used to define a function type by ensuring a function signature. We use the optional property using a question mark before the property name colon. This optional property indicates that objects belonging to the Interface may or may not have to define these properties.

How do you overload a function in TypeScript?

Function overloading with different number of parameters and types with same name is not supported. Thus, in order to achieve function overloading, we must declare all the functions with possible signatures. Also, function implementation should have compatible types for all declarations.

How do you return a value from a TypeScript function?

The function's return type is string. Line function returns a string value to the caller. This is achieved by the return statement. The function greet() returns a string, which is stored in the variable msg.


1 Answers

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;
like image 194
Titian Cernicova-Dragomir Avatar answered Oct 08 '22 12:10

Titian Cernicova-Dragomir