更干净的动态打字稿 class 方法

Cleaner dynamic typescript class methods

我正在将一个大型项目从流程转换为 TS,但我无法弄清楚如何创建动态 class 方法。我拼凑了一个简单的工作示例来说明我正在尝试做的事情:

ts playground link

declare type logMethod = (msg: string) => void;

class Logger {
  constructor(methods: string[]) {
    methods.forEach(method => this[method] = (msg: string) => Logger.logMethod.call(this, method, msg))
  }

  static logMethod(method: string, msg: string) {}

  [key: string]: any;
}

function LoggerFactory<T extends string>(methods: T[]): Logger & Record<T, logMethod> {
  const logger = new Logger(methods);
  return logger as Logger & Record<T, logMethod>;
}

const logger = LoggerFactory(['info', 'warn', 'error']);
logger.info('info');
logger.warn('warn');
logger.error('error');

这工作得很好,因为在 infowarnerror 或我传递给工厂的任何其他方法名称都被正确键入。但是我讨厌为了让类型系统开心而使用额外的 JS。 LoggerFactory 在 JS 中完全没有必要,我正在想办法摆脱它。

我正试图走这样的路:

class Logger<T extends string[]> {
  [method in keyof T]: logMethod;
}

但该映射语法不适用于 classes 或接口,仅适用于普通类型。只是在这里寻找想法!

TypeScript 要求所有 classinterface 类型都具有静态已知键。因此无法注释 class Logger<K extends string> {} 以使其具有 K 类型的键。您绝对可以描述使用动态键创建实例的 class 构造函数的类型,但是您将无法将这种类型应用于声明为 class 语句的内容。

可以 做的是将 class 重命名为 class _Logger {...},使其尽可能接近您想要的行为。然后你可以写 var Logger = _Logger as LoggerConstructorassert 它是所需的类型 LoggerConstructor,你将定义它以便它的实例具有你想要的动态键。

这是一种方法:

class _Logger {
  constructor(methods: string[]) {
    methods.forEach(method =>
      (this as Logger<any>)[method] = (msg: string) =>
        Logger.logMethod.call(this, method, msg))
  }
  foo() { return 42; }
  static logMethod(method: string, msg: string) {
    console.log("method: " + method + ", msg: " + msg)
  }
}

type Logger<K extends string> = Record<K, LogMethod> & _Logger;

interface LoggerConstructor {
  new <K extends string>(methods: K[]): Logger<K>;
  logMethod(method: string, msg: string): void;
}

var Logger = _Logger as LoggerConstructor;

_Logger class 可以按照你想要的方式工作,但它只知道 foo() 作为实例方法和 logMethod() 作为静态方法。您希望 Logger<K> 实例同时充当 LoggerRecord<K, LogMethod>,因此您可以这样定义它(使用 [intersection])(https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types). And finally, you can define LoggerConstructor as having a generic construct signature生成 Logger<K> 的实例(以及具有您希望在构造函数中看到的任何静态属性)。

现在我们可以测试它了:

const logger = new Logger(['info', 'warn', 'error']);
// const logger: Logger<"info" | "warn" | "error">
logger.info('Info'); // method: info, msg: Info
logger.warn('Warn'); // method: warn, msg: Warn
logger.error('Error'); // method: error, msg: Error
console.log(logger.foo().toFixed(2)); // 42.00
logger.oops('Oops'); // error
// --> ~~~~ Property 'oops' does not exist

看起来不错!编译器知道 loggerLogger<"info" | "warn" | "error"> 类型,因此 loggerinfowarnerror 方法。它还知道 _Logger 中的 foo(),如果您尝试调用不存在的 oops 之类的方法,它会抱怨。

Playground link to code