我可以在 Typescript 中重用函数的参数定义吗? [英] Can I reuse the parameter definition of a function in Typescript?

查看:29
本文介绍了我可以在 Typescript 中重用函数的参数定义吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想捕获一个函数的编译时参数结构,我可以在具有相似签名的多个函数定义中重用该结构.我认为这可能与 这个 TS 问题 类似,或者更具体地说 这个,但我不确定我的用例是否一定符合这些建议,所以在当前的 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.

推荐答案

您可以定义一个包含所有函数定义的额外接口.并使用映射类型将这些函数转换为on 的签名和emit 的签名.两者的过程有点不同,所以我会解释一下.

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
}

对于on,我们想创建新函数,将接口中的属性名称作为第一个参数,第二个参数是函数本身.为此,我们可以使用映射类型

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 }

对于emit,我们需要给每个函数添加一个参数,即事件名称,我们可以使用这个答案

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

现在我们已经转换了原始界面,我们需要将所有功能合并为一个.要获得所有签名,我们可以使用 T[keyof T](即 EmitSignatures[keyof Events])但这将返回所有签名的联合,这不会被调用.这是一个有趣的类型来自 this 表单中的答案UnionToIntersection 将我们的签名联合转换为所有签名的交集.

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.

综合起来,我们得到:

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

特别感谢@jcalz 提供了一个拼图

Special thanks to @jcalz for a piece of the puzzle

编辑如果我们已经有一个具有 onemit 的非常通用的实现的基类,我们需要对类型系统进行一些处理.

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

编辑 3.0

自从撰写本文以来,typescript 已经改进了映射函数的能力.使用 其余参数和传播表达式中的元组,我们可以替换 的多个重载AddParameters 具有更清晰的版本(不需要 IsValidArg):

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;

这篇关于我可以在 Typescript 中重用函数的参数定义吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆