更干净的动态打字稿 class 方法
Cleaner dynamic typescript class methods
我正在将一个大型项目从流程转换为 TS,但我无法弄清楚如何创建动态 class 方法。我拼凑了一个简单的工作示例来说明我正在尝试做的事情:
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');
这工作得很好,因为在 info
、warn
、error
或我传递给工厂的任何其他方法名称都被正确键入。但是我讨厌为了让类型系统开心而使用额外的 JS。 LoggerFactory
在 JS 中完全没有必要,我正在想办法摆脱它。
我正试图走这样的路:
class Logger<T extends string[]> {
[method in keyof T]: logMethod;
}
但该映射语法不适用于 classes 或接口,仅适用于普通类型。只是在这里寻找想法!
TypeScript 要求所有 class
或 interface
类型都具有静态已知键。因此无法注释 class Logger<K extends string> {}
以使其具有 K
类型的键。您绝对可以描述使用动态键创建实例的 class 构造函数的类型,但是您将无法将这种类型应用于声明为 class
语句的内容。
您 可以 做的是将 class 重命名为 class _Logger {...}
,使其尽可能接近您想要的行为。然后你可以写 var Logger = _Logger as LoggerConstructor
到 assert 它是所需的类型 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>
实例同时充当 Logger
和 Record<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
看起来不错!编译器知道 logger
是 Logger<"info" | "warn" | "error">
类型,因此 logger
有 info
、warn
和 error
方法。它还知道 _Logger
中的 foo()
,如果您尝试调用不存在的 oops
之类的方法,它会抱怨。
我正在将一个大型项目从流程转换为 TS,但我无法弄清楚如何创建动态 class 方法。我拼凑了一个简单的工作示例来说明我正在尝试做的事情:
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');
这工作得很好,因为在 info
、warn
、error
或我传递给工厂的任何其他方法名称都被正确键入。但是我讨厌为了让类型系统开心而使用额外的 JS。 LoggerFactory
在 JS 中完全没有必要,我正在想办法摆脱它。
我正试图走这样的路:
class Logger<T extends string[]> {
[method in keyof T]: logMethod;
}
但该映射语法不适用于 classes 或接口,仅适用于普通类型。只是在这里寻找想法!
TypeScript 要求所有 class
或 interface
类型都具有静态已知键。因此无法注释 class Logger<K extends string> {}
以使其具有 K
类型的键。您绝对可以描述使用动态键创建实例的 class 构造函数的类型,但是您将无法将这种类型应用于声明为 class
语句的内容。
您 可以 做的是将 class 重命名为 class _Logger {...}
,使其尽可能接近您想要的行为。然后你可以写 var Logger = _Logger as LoggerConstructor
到 assert 它是所需的类型 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>
实例同时充当 Logger
和 Record<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
看起来不错!编译器知道 logger
是 Logger<"info" | "warn" | "error">
类型,因此 logger
有 info
、warn
和 error
方法。它还知道 _Logger
中的 foo()
,如果您尝试调用不存在的 oops
之类的方法,它会抱怨。