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); }; }; }
关键改进点
- 泛型推断顺序调整:让装饰器的返回函数先推断被装饰函数的
TReq类型,再通过infer TParams提取出params的类型。 - 类型约束后置:在返回函数内部通过
AllowedParams约束传入的required参数必须是TParams的键,此时TypeScript会根据被装饰函数的request参数类型进行校验。 - 类型断言兼容:使用
as readonly AllowedParams[]确保类型校验通过,同时保留运行时的数组值。
验证效果
使用开头的测试代码,@requiredParams('id', 'name')会触发类型错误,提示'name'不能赋值给keyof Params类型,符合预期。
内容的提问来源于stack exchange,提问作者tdranv
相关产品推荐
相关产品推荐

