网站搜索

TypeScript 中的类封装函数


这是我的班级:

class Foo {
  static actions = ["func1", "func2"];
  a: number;
  func1(str: string) {
  };
  func2(n: number) {
  };
  func3() {}
}

我的目标是为一个泛型函数(下面的 wrapIt 函数)做出正确的 TypeScript 注解,该函数接受任何类(例如 Foo,具有一组随机的属性和函数)并返回一个只实现静态字段 actions 中指定的操作的类:

function wrapIt(MyClass) {
  return class {
    constructor() {
      for (const actionName of MyClass.actions) {
        const fn = MyClass.prototype[actionName];
        this[actionName] = (...args: any[]) => {
          console.log("do something here that just call the original function");
          return fn.bind(this)(...args);
        }
      }
    }
  }
}

const NewClass = wrapIt(Foo)

const c = new NewClass();
c.func1("ok"); // typescript error: func1 is not known

Result 的 func1 和 func2 可能是原始的 func1 和 func2,但被一些代码包裹着

我用纯 JavaScript 编写这种代码没有困难,但在 TypeScript 中,对我来说简直是地狱 :-|

TS 游乐场

答案#1

你需要通过添加 as const 来将 Foo 中的 actions 设为编译时常量:

class Foo {
    // ...
    static actions = ["func1", "func2"] as const;
    // ...                             ^^^^^^^^^
}

然后我们可以在 wrapIt 上使用泛型类型参数。首先让我们为构造函数定义一个可重用的类型:

type ConstructorFunction = new (...args: any[]) => any;

现在我们需要定义 wrapIt 参数的类型。它需要是:

  • 构造函数
  • 具有一个 actions 属性,该属性是一个有效方法名称的数组

我们需要将方法名称作为它自己的类型参数。为了限制它们,我们需要让它们扩展由构造函数创建的对象类型的 keyof,我们可以通过实用类型 InstanceType 来获取。它看起来像这样:

function wrapIt<Cls extends ConstructorFunction, Method extends keyof InstanceType<Cls>>(
    MyClass: Cls & { actions: readonly Method[] }
) {
    // ...
}

(我实际上并没有在那里将 actions 限制为仅 method 名称,只限制了实例属性名称。如果你不希望用户在 actions 中写 "a",你可以进一步处理,但我在这里没有处理。不过,我在下面包含了关于如何操作的说明。)

现在来说返回类型。使用这两个泛型参数和 Pick 工具类型,返回类型相对简单:Pick<InstanceType<Cls>, Method>。让我们将其定义为可以在多个地方使用的内容:

type WrapItReturn<Cls extends ConstructorFunction, Method extends keyof InstanceType<Cls>> = new (
    ...args: any
) => Pick<InstanceType<Cls>, Method>;

然后我们使用 WrapItReturn<Cls, method> 作为返回类型(仅此,我们可以让 TypeScript 推断它,但让我们显式指定)以及作为我们返回内容的类型断言:

function wrapIt<Cls extends ConstructorFunction, Method extends keyof InstanceType<Cls>>(
    MyClass: Cls & { actions: readonly Method[] }
): WrapItReturn<Cls, Method> {
    return class {
        constructor() {
            for (const actionName of MyClass.actions) {
                const fn = MyClass.prototype[actionName];
                (this as any)[actionName] = (...args: any[]) => {
                    console.log("do something here that just call the original function");
                    return fn.call(this, ...args); // Minor implementation tweak here
                };
            }
        }
    } as WrapItReturn<Cls, Method>;
}

这一切都在一起如下:

type ConstructorFunction = new (...args: any[]) => any;

class Foo {
    static actions = ["func1", "func2"] as const;
    a!: number; // Added the ! just to avoid irrelevant initialization error, not part of solution
    func1(str: string) {
        console.log(str);
    }
    func2(n: number) {
        console.log(n);
    }
    func3() {}
}

type WrapItReturn<Cls extends ConstructorFunction, Method extends keyof InstanceType<Cls>> = new (
    ...args: any
) => Pick<InstanceType<Cls>, Method>;

function wrapIt<Cls extends ConstructorFunction, Method extends keyof InstanceType<Cls>>(
    MyClass: Cls & { actions: readonly Method[] }
): WrapItReturn<Cls, Method> {
    return class {
        constructor() {
            for (const actionName of MyClass.actions) {
                const fn = MyClass.prototype[actionName];
                (this as any)[actionName] = (...args: any[]) => {
                    console.log("do something here that just call the original function");
                    return fn.call(this, ...args); // Minor implementation tweak here
                };
            }
        }
    } as WrapItReturn<Cls, Method>;
}

const NewClass = wrapIt(Foo);

const c = new NewClass();
c.func1("ok"); // typescript error: func1 is not known

游乐场链接


如果你想将 actions 的内容限制为仅 methods 的键,你可以使用这个工具类型:

type FunctionKeys<ObjType> = keyof {
    [Key in keyof ObjType as ObjType[Key] extends (...args: any[]) => any ? Key : never]: Key;
};

然后使用 FunctionKeys<InstanceType<Cls>> 替代 keyof InstanceType<Cls>:

type WrapItReturn<Cls extends ConstructorFunction, Method extends FunctionKeys<InstanceType<Cls>>> = new (
    ...args: any
) => Pick<InstanceType<Cls>, Method>;

function wrapIt<Cls extends ConstructorFunction, Method extends FunctionKeys<InstanceType<Cls>>>(
    MyClass: Cls & { actions: readonly Method[] }
): WrapItReturn<Cls, Method> {
    // ...
}

游乐场链接

在这种情况下,如果你向 actions 添加 "a",你会看到 Foo 不再是 wrapIt 的有效参数,因为一个非函数属性被列在了 actions 中。