我正在尝试编写一个日志记录装饰器工厂,我可以在类、方法和箭头函数字段中重用它。我正在使用 TS 5.6,不确定它们是否包含更新的类型。我已经使用关于装饰器的TS博客文章和2ality的参考文章让它在箭头函数字段中工作,但是当我将其扩展为也支持类和方法装饰器时,我似乎无法让它工作基于“种类”的条件:
type Constructor<T = object> = new (...args: unknown[]) => T;
// factory function that returns a decorator
export function log<This, Value, Args extends unknown[], Return>(
prefix = '[LOG]'
) {
// return a decorator function
return function (value: Value | undefined, context: ClassFieldDecoratorContext | ClassMethodDecoratorContext | ClassDecoratorContext) {
const name = String(context.name);
// class and methods, value is a constructor or a function
if (context.kind === 'class') {
const OriginalClass = value as Constructor;
return class extends OriginalClass {
constructor(...args: unknown[]) {
console.log(`${prefix} >> ${name}`);
console.log(`${prefix} -- ${name} Args: ${JSON.stringify(args)}`);
super(...args);
console.log(`${prefix} << ${name}`);
}
}
}
if (context.kind === 'method') {
// with methods you can replace the original function
return function (this: This, ...args: Args): Return {
console.log(`${prefix} >> ${name}`);
console.log(`${prefix} -- ${name} Args: ${JSON.stringify(args)}`);
const result = (value as (...args: unknown[]) => unknown).apply(this, args) as Return;
console.log(`${prefix} << ${name} = ${JSON.stringify(result)}`);
return result;
};
}
// class fields, value is undefined
if (context.kind === 'field') {
// with class fields you can only replace the initial value
return function (this: This, initialValue: Value) {
if (typeof initialValue === 'function') {
// if the value is an arrow function, return a new function
return function (this: This, ...args: Args): Return {
console.log(`${prefix} >> ${name}`);
console.log(`${prefix} -- ${name} Args: ${JSON.stringify(args)}`);
const result = initialValue.apply(this, args) as Return;
console.log(`${prefix} << ${name} = ${JSON.stringify(result)}`);
return result;
};
}
// fallback to the initial value
return initialValue;
};
}
// fallback to the original value
return value;
};
}
@log('[CLASS]')
class Hello {
#name: string;
constructor(private name?: string) {
this.#name = name ?? 'World!';
}
@log('[METHOD]')
method() {
return `Hello, ${this.#name}!`;
}
@log('[AFF]')
aff = () => `Hello, ${this.#name}!`;
}
// check for correct value and binding
const hello = new Hello('Name!');
const aff = hello.aff;
console.log(hello.method());
console.log(hello.aff());
console.log(aff());
它似乎表现正确,但在我的 IDE 中弹出的 TS 错误是沿着这些线的,我不知道如何修复:
Decorator function return type 'typeof Hello | typeof (Anonymous class) | ((this: unknown, ...args: unknown[]) => unknown) | ((this: unknown, initialValue: typeof Hello) => typeof Hello | ((this: unknown, ...args: unknown[]) => unknown)) | undefined' is not assignable to type 'void | typeof Hello'.
Type 'typeof (Anonymous class)' is not assignable to type 'void | typeof Hello'.
Types of construct signatures are incompatible.
Type 'new (...args: unknown[]) => log<unknown, typeof Hello, unknown[], unknown>.(Anonymous class)' is not assignable to type 'new (name?: string | undefined) => Hello'.
Type 'log<unknown, typeof Hello, unknown[], unknown>.(Anonymous class)' is missing the following properties from type 'Hello': #name, method, aff. ts(1270)
函数中的大多数generic类型参数根本没有任何好处。 TS 对 ES Decorators 的支持仍然不支持任何类型的 mutation 装饰事物的类型(因此 microsoft/TypeScript#4881 仍然开放),因此由
返回的函数输出的类型log()
实际上只能是其第一个参数或 void
的类型。 由于函数的实现使用 类型断言 来表示函数属于与泛型类型参数相关的类型,并且由于这些类型参数是在 log()
上声明的,而不是在它返回的函数上声明的,因此实际上它们是什么都不做。
唯一重要的泛型类型参数是
Value
。您希望 log()
返回的函数接受 Value
类型的输入并返回 Value
类型的输出。所有其他类型都取决于您,但这是重要的一点:
function log(prefix = '[LOG]') {
return function <Value>(
value: Value,
context: ClassFieldDecoratorContext | ClassMethodDecoratorContext | ClassDecoratorContext
): Value {
// impl
}
}
请注意,我将泛型类型参数的范围从
log
移至返回的函数。因此,您不会要求 TypeScript 使用 contextualtyping 来推断 Value
。 如果你写的话,你现在的方式就可以正常工作
@log('[CLASS]')
class Hello {
}
但如果你写表面上等价的就会失败
const logClass = log('[CLASS]');
@logClass
class Hello {
}
如果将
Value
移至返回的函数,则上面的 logClass
仍将是通用的,并且当您使用 Value
装饰器时将推断 @logClass
,而不是因 Value
为 unknown
而失败。
这就是
log()
在呼叫者看来应该是这样的。 至于实现,您可能需要在 return 语句上使用类型断言,以便 TS 接受它的类型 Value
,但同样,您已经在使用它们,因此它并没有真正失去任何类型安全性:
function log(prefix = '[LOG]') {
// return a decorator function
return function <Value>(
value: Value,
context: ClassFieldDecoratorContext | ClassMethodDecoratorContext | ClassDecoratorContext
): Value {
if (context.kind === 'class') {
const OriginalClass = value as Constructor;
return class extends OriginalClass { ⋯ } as Value
}
if (context.kind === 'method') {
return function (this: any, ...args: any) { ⋯ } as Value;
}
if (context.kind === 'field') {
return function (this: any, initialValue: Value) { ⋯ } as Value;
}
return value;
};
}
同样,您可以使用
any
或 Function
以外的类型,或任何您需要的类型。您可能会在某个时候决定使用更强的类型,但是装饰器不需要从调用站点工作。