创建参数化 TypeScript 5 方法装饰器映射参数时出现问题

问题描述 投票:0回答:1

我想创建一个带有参数的 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;
typescript typescript-decorator
1个回答
0
投票

在试图理解你的

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 问题才能确定(但如果您这样做,我强烈建议将代码削减为证明问题的绝对最小值)。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.