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

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); // 现在也能正常使用

关键修改点

  1. 添加readonly约束:将ExtractAssertions和multiAssert的数组泛型约束改为readonly Asserter<T, any>[],匹配TypeScript对数组字面量的默认推断类型。
  2. 明确Rest类型:在递归条件中直接将Rest约束为readonly Asserter<T, any>[],避免后续类型判断导致的推断失败,确保递归能正确提取每个断言的类型并交叉合并。

修改后,multiAssert可正确将data的类型收窄为Base & WithName & WithAge,支持后续对name和age的无错误访问。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 13:18:13