TypeScript断言列表类型收窄:ExtractAssertions类型定义问题排查
类型安全的批量断言实现问题及解决
问题描述
我想要以类型安全的方式组合断言列表,无需手动逐个调用断言。示例代码中定义了Base、WithName等类型,以及Asserter类型和对应的assertName、assertAge断言函数,还有multiAssert函数用于批量执行断言。但ExtractAssertions类型的定义存在问题,尝试多种递归写法后,要么返回Base类型,要么得到never类型,无法实现预期的类型收窄效果。
示例代码
type Base = { id: string, name?: string, age?: number }; type WithName = { name: string }; type WithAge = { age: number }; export type Asserter<T, U> = (x: T) => asserts x is T & U; const assertName: Asserter<Base, WithName> = (x: Base): asserts x is Base & WithName => { if (x.name === undefined) { throw new Error("missing name"); } }; const assertAge: Asserter<Base, WithAge> = (x: Base): asserts x is Base & WithAge => { if (x.age === undefined) { throw new Error("missing age"); } }; type ExtractAssertions<T, U extends Asserter<T, any>[]> = U extends [Asserter<T, infer V>, ...infer Rest] ? Rest extends Asserter<T, any>[] ? V & ExtractAssertions<T, Rest> : V : {}; function multiAssert<T, A extends Asserter<T, any>[]>( item: T, assertions: A ): asserts item is T & ExtractAssertions<T, A> { assertions.forEach(assertion => assertion(item)); } const data: Base = { id: "aas-aa", name: "frank", age: 30 }; multiAssert(data, [assertName, assertAge]); console.log(data.name[0]); // 期望能正常编译 console.log(data.age + 3); // 期望也能正常使用
解决方案
问题出在ExtractAssertions的类型约束和递归逻辑上:TypeScript对数组字面量的默认推断是readonly数组,原类型未考虑这一点导致类型匹配失败;同时递归时的Rest类型推断不够明确,需直接约束为断言数组类型。
修正后的代码如下:
type Base = { id: string, name?: string, age?: number }; type WithName = { name: string }; type WithAge = { age: number }; export type Asserter<T, U> = (x: T) => asserts x is T & U; const assertName: Asserter<Base, WithName> = (x: Base): asserts x is Base & WithName => { if (x.name === undefined) { throw new Error("missing name"); } }; const assertAge: Asserter<Base, WithAge> = (x: Base): asserts x is Base & WithAge => { if (x.age === undefined) { throw new Error("missing age"); } }; // 修正ExtractAssertions:添加readonly约束,明确Rest的类型 type ExtractAssertions<T, U extends readonly Asserter<T, any>[]> = U extends readonly [Asserter<T, infer V>, ...infer Rest extends readonly Asserter<T, any>[]] ? V & ExtractAssertions<T, Rest> : {}; // 修正multiAssert的泛型约束为readonly数组 function multiAssert<T, A extends readonly Asserter<T, any>[]>( item: T, assertions: A ): asserts item is T & ExtractAssertions<T, A> { assertions.forEach(assertion => assertion(item)); } const data: Base = { id: "aas-aa", name: "frank", age: 30 }; multiAssert(data, [assertName, assertAge]); console.log(data.name[0]); // 现在可以正常编译 console.log(data.age + 3); // 现在也能正常使用
关键修改点
- 添加
readonly约束:将ExtractAssertions和multiAssert的数组泛型约束改为readonly Asserter<T, any>[],匹配TypeScript对数组字面量的默认推断类型。 - 明确Rest类型:在递归条件中直接将
Rest约束为readonly Asserter<T, any>[],避免后续类型判断导致的推断失败,确保递归能正确提取每个断言的类型并交叉合并。
修改后,multiAssert可正确将data的类型收窄为Base & WithName & WithAge,支持后续对name和age的无错误访问。
内容的提问来源于stack exchange,提问作者Daan Reid
相关产品推荐
相关产品推荐

