我想创建一个带有参数的 TypeScript 5 方法装饰器,用于记录目的。
参数
mappers
可以允许将初始函数参数映射到任意值。
我的第一个困难是从方法参数创建类型。
我发现这篇很棒的文章(注意:已经过时了,因为它是在可变元组存在之前写的) 并最终得到了一个工作版本(至少我认为它有效)。
type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
type Tail<T extends any[]> = ((...t: T) => any) extends (_: any, ...tail: infer TT) => any ? TT : [];
type Length<T extends any[]> = T['length'];
type IsRest<T, P extends any[]> = Required<T> extends never ? number extends Length<P> ? true : false : false;
type ParamsToFunc<T extends any[], R = any, RT extends any[] = Required<T>> = [
...(
IsRest<Head<T>, T> extends true
? [(...v: T) => R]
: Head<T> extends never
? Head<RT> extends never
? []
: [(v?: Head<RT>) => R]
: [(v: Head<T>) => R]
),
...(
IsRest<Head<T>, T> extends true
? []
: Length<Tail<T>> extends 0
? []
: ParamsToFunc<Tail<T>, R>
),
];
这是测试
type T01 = ParamsToFunc<Parameters<() => string>>;
// []
type T02 = ParamsToFunc<Parameters<(a: string) => string[]>>;
// [(v: string) => any]
type T03 = ParamsToFunc<Parameters<(a: string[]) => number>>;
// [(v: string[]) => any]
type T04 = ParamsToFunc<Parameters<(a: undefined, b: string[]) => number>>;
// [(v: undefined) => any, (v: string[]) => any]
type T05 = ParamsToFunc<Parameters<(a: undefined, ...rest: string[]) => number>>;
// [(v: undefined) => any, (...v: string[]) => any]
type T06 = ParamsToFunc<Parameters<(...rest: string[]) => number>>;
// [(...v: string[]) => any]
type T07 = ParamsToFunc<Parameters<(a: string[], ...rest: string[]) => number>>;
// [(v: string[]) => any, (...v: string[]) => any]
type T08 = ParamsToFunc<Parameters<(a: string, b: number) => number>>;
// [(v: string) => any, (v: number) => any]
type T09 = ParamsToFunc<Parameters<(a: string, b: number, ...rest: string[]) => number>>;
// [(v: string) => any, (v: number) => any, (...v: string[]) => any]
type T10 = ParamsToFunc<Parameters<(options?: boolean) => Promise<number>>>;
// [(v?: boolean | undefined) => any]
type T11 = ParamsToFunc<Parameters<(options?: boolean, other?: boolean) => Promise<number>>>;
// [(v?: boolean | undefined) => any, (v?: boolean | undefined) => any]
type T12 = ParamsToFunc<Parameters<(a: string | number, to: string, options?: boolean) => Promise<number>>>;
// [(v: string | number) => any, (v: string) => any, (v?: boolean | undefined) => any]
到目前为止一切顺利。该类型处理棘手的情况,例如可选参数或其余参数。
现在,这是我的方法装饰器工厂的简化版本(使其毫无用处)。
type TypedFunction<This, Args extends any[], Return> = (this: This, ...args: Args) => Return;
type LoggedMethodOptions<Args extends any[]> = { mappers: ParamsToFunc<Args> };
function loggedMethod<This, Args extends any[], Return>(_options?: LoggedMethodOptions<Args>) {
return function (
target: TypedFunction<This, Args, Return>,
_context: ClassMethodDecoratorContext<This, TypedFunction<This, Args, Return>>
) {
return function (this: This, ...args: Args): Return {
return target.call(this, ...args);
};
};
}
花时间创建合适的类型后,我预计一切都会好起来,但我错了。
type FileId = string;
class FileService {
// OK
@loggedMethod({ mappers: [v => v.toUpperCase()] })
deleteFile(_id: FileId): void {}
// error "Parameter 'v' implicitly has an 'any' type.(7006)" for all mappers
// however I see
// (property) mappers: [(v: string) => any, (v: string) => any, (v?: { verbose?: boolean; } | undefined) => any]
@loggedMethod({ mappers: [v => v.toUpperCase(), v => v.toUpperCase(), v => v?.verbose] })
copyFile(_from: FileId, _to: FileId, _options?: { verbose?: boolean }): void { }
// OK (makes me sick because `copyFile` seems so close)
@loggedMethod({ mappers: [v => v.toUpperCase(), v => v.toUpperCase(), v => v.toUpperCase()] })
copyFile2(_from: FileId, _to: FileId, _3rdParty: FileId): void { }
// error "Parameter 'v' implicitly has an 'any' type.(7006)" for all mappers
// however I see
// property) mappers: [(v: string) => any, (v: any[]) => any]
@loggedMethod({ mappers: [v => v.toUpperCase(), v => v.length] })
saveToFile(_to: FileId, _contents: any[]): void { }
// OK
@loggedMethod({ mappers: [v => v.toUpperCase()] })
getFileAndMetadata(_desc: FileId): void { }
}
似乎 TypeScript 认为映射器输入是类型
any
只要存在不同类型的初始参数。
因此,
copyFile
有错误(FileId
+ FileId
+ 其他),而 copyFile2
没有错误(FileId
+ FileId
+ FileId
)。尽管如此,mappers
显示的类型似乎是正确的。
这里是一个 TypeScript Playground,其中包含上述所有代码(TypeScript 5.6.3 / 目标 ES2022)。
我不明白为什么当我将鼠标悬停在定义上时类型看起来没问题,同时 TypeScript 却抱怨。
感谢@jcalz提供了一个工作示例,使用了我的类型的更简洁(和简单)版本。
type ParamsToFunc<T extends any[], A extends any[] = []> =
T extends [infer F, ...infer R] ? ParamsToFunc<R, [...A, (v: F) => any]> :
T extends [] ? A :
T extends [(infer F)?, ...infer R] ? (
T extends R ? [...A, (...v: T) => any] : ParamsToFunc<R, [...A, (v?: F) => any]>
) : A;
在试图理解你的
ParamsToFunc<T>
的作用时,我致力于编写自己的更“直接”版本(是否实际上更直接是有争议的;我的没有定义额外的实用程序类型):
type ParamsToFunc<T extends any[], A extends any[] = []> =
T extends [infer F, ...infer R] ? ParamsToFunc<R, [...A, (v: F) => any]> :
T extends [] ? A :
T extends [(infer F)?, ...infer R] ? (
T extends R ? [...A, (...v: T) => any] : ParamsToFunc<R, [...A, (v?: F) => any]>
) : never;
此版本运行没有错误。它重现了测试用例,以及回调参数的上下文类型,例如
@loggedMethod({ mappers: [v => v.toUpperCase(), v => v.toUpperCase(), v => v.toUpperCase()] })
copyFile2(_from: FileId, _to: FileId, _3rdParty: FileId): void { }
发生正常;没有“隐式
any
”错误。
我将简要描述我的
ParamsToFunc
版本,我首先检查 T extends [infer F, ...infer R]
以查看它是否是具有所需第一个元素的 tuple;如果是这样,我们递归并将“plain”函数添加到累加器A
。否则我检查 T extends []
看看我们是否已经到达元组的末尾;如果是,我们返回累加器A
。 否则,我检查 T extends [(infer F)?, ...infer R]
以查看元组是否具有 可选 元素。无论如何,这实际上都是正确的;所以接下来的检查是 T extends R
来查看数组的其余部分和当前数组是否相同。如果是这样,那么我们就击中了一个普通数组或一个 rest element,因为从数组中拉出第一个元素不会改变它。 如果我们击中了剩余元素,那么我们将返回累加器,并在末尾带有“剩余”函数。否则,我们递归并将“可选”函数添加到累加器A
。然后我们遇到了不可能的情况,其中 T extends [(infer F)?, ...infer R]
为假,所以我们返回 never
。
所以这个版本可以工作,但它不一定能回答为什么你的版本不行。
回到你的定义,我可以对其进行的最小更改是:
type ParamsToFunc<T extends any[], R = any, RT extends any[] = Required<T>> =
IsRest<Head<T>, T> extends true ? [(...v: T) => R] :
[...(
Head<T> extends never ? (
Head<RT> extends never ? [] : [(v?: Head<RT>) => R]
) : [(v: Head<T>) => R]
), ...(
Length<Tail<T>> extends 0 ? [] : ParamsToFunc<Tail<T>, R>
)];
我已将您的
IsRest<Head<T>, T>
测试从一对 variadic tuple 部分中移出,将它们组合成一个测试,并将其放在第一位。 其余代码本质上是相同的。
再次强调,测试用例是相同的,但现在回调参数始终根据上下文键入。
这个still并没有回答为什么你的版本不起作用,但它显示了问题的位置。
此时您有两个工作版本,但没有明确的答案为什么另一个版本不起作用。我的猜测是,由于 decorators 必须根据上下文从函数的预期 return 类型 推断其 generic 类型参数,因此推断的顺序可能会有所不同,具体取决于所涉及的类型。您的原始版本始终使用可变参数元组,即使对于非递归/基本情况也是如此。更新后的版本没有。我不清楚为什么这很重要。
有关于类似问题的 GitHub 问题,例如 microsoft/TypeScript#52047,但我不能肯定这是相同的基本情况。这被认为是 TypeScript 中的一个错误,但修复起来可能并不容易。
就这样吧。如果这个问题是“我怎样才能让它发挥作用”,我已经回答了。如果主要是“为什么这不起作用”,那么我只是发布了猜测,您可能需要提交一个 GitHub 问题才能确定(但如果您这样做,我强烈建议将代码削减为证明问题的绝对最小值)。