You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript:从被装饰函数类型推导装饰器参数的类型安全约束

构建类型安全的参数校验装饰器

需求:实现一个类型安全的装饰器requiredParams,它接受的字符串数组元素必须是被装饰函数的request.params对象的键名。例如:

type Params = {id: string};

class Test {
  @requiredParams('id', 'name') // 此处应触发类型错误,因为`name`不是Params的键
  async handler(request: IRequest<void, Params>): Promise<void> {}
}

现有实现问题

当前的实现无法正确推断被装饰函数的TRequestParams类型,导致类型约束失效。原代码如下:

export function requiredParams<
    TRequestParams extends Record<string, string>,
    TRequiredRequestParams extends keyof TRequestParams & string
>(...requiredRequestParams: TRequiredRequestParams[]) {
    return function <
        TThis,
        TArguments extends Parameters<AsyncRequestHandler<any, TRequestParams, any>>,
        TReturn
    >(
        target: (this: TThis, ...args: TArguments) => Promise<TReturn>,
        context: ClassMethodDecoratorContext<TThis, (this: TThis, ...args: TArguments) => Promise<TReturn>>,
    ) {
        return async function(this: TThis, ...args: TArguments): Promise<TReturn> { 
            // 校验逻辑示例
            const params = args[0].params;
            for (const param of requiredRequestParams) {
                if (!(param in params)) {
                    throw new Error(`缺少必填参数:${param}`);
                }
            }
            return target.apply(this, args);
        };
    };
}

依赖的类型定义:

type AsyncRequestHandler<
    TBody = any,
    TParams = any,
    TQuery = any
> = (request: IRequest<TBody, TParams, TQuery>, response: IResponse, next?: Next) => Promise<void>;

interface IRequest<TBody = any, TParams = any, TQuery = any> {
    body: TBody;
    params: TParams;
    query: TQuery;
}

// 补充缺失的基础类型
interface IResponse {}
type Next = () => void;

修正后的实现

问题核心在于泛型推断顺序错误,应该让requiredParams从被装饰函数的参数中推断TRequestParams,而非提前声明。调整后的代码:

export function requiredParams<
    TRequired extends string[]
>(...required: TRequired) {
    return function <
        TThis,
        TReq extends IRequest<any, infer TParams, any>,
        TArgs extends [TReq, IResponse, Next?],
        TReturn
    >(
        target: (this: TThis, ...args: TArgs) => Promise<TReturn>,
        context: ClassMethodDecoratorContext<TThis, (this: TThis, ...args: TArgs) => Promise<TReturn>>,
    ) {
        // 约束传入的参数必须是TParams的键
        type AllowedParams = keyof TParams & string;
        const _required = required as readonly AllowedParams[];

        return async function(this: TThis, ...args: TArgs): Promise<TReturn> {
            const params = args[0].params;
            for (const param of _required) {
                if (!(param in params)) {
                    throw new Error(`缺少必填参数:${param}`);
                }
            }
            return target.apply(this, args);
        };
    };
}

关键改进点

  1. 泛型推断顺序调整:让装饰器的返回函数先推断被装饰函数的TReq类型,再通过infer TParams提取出params的类型。
  2. 类型约束后置:在返回函数内部通过AllowedParams约束传入的required参数必须是TParams的键,此时TypeScript会根据被装饰函数的request参数类型进行校验。
  3. 类型断言兼容:使用as readonly AllowedParams[]确保类型校验通过,同时保留运行时的数组值。

验证效果

使用开头的测试代码,@requiredParams('id', 'name')会触发类型错误,提示'name'不能赋值给keyof Params类型,符合预期。

内容的提问来源于stack exchange,提问作者tdranv

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 14:03:00