PAYATHON 2026

为什么 TypeScript 能收窄 param[0],却不能收窄 param.at(0)?

数据小许

结论

在这段代码中,TypeScript 能跟踪 param[0] 这样的索引访问,并在分支内将条件判断得到的类型信息用于同一访问位置。

param.at(0) 是方法调用。再次写出它会产生一次新的调用,编译器不会把前一次返回值的类型收窄结果用于后一次返回值。可以先保存 at(0) 的返回值,再判断这个变量。

为什么 param[0] 可以收窄

if (Array.isArray(param[0])) {
  const item: ListItem[] = param[0];
}

param[0] 使用固定索引,是 TypeScript 能够进行控制流类型收窄的访问形式。分支中没有中途赋值,所以编译器可以关联条件里的 param[0] 和后面的 param[0]。

原来的元素类型是 ListItem | ListItem[]。经过 Array.isArray 检查后,分支内该元素被收窄为 ListItem[]。

这不意味着整个 param 被收窄成了 ListItem[][],其他索引也不会因此获得相同的类型。

为什么 param.at(0) 不行

if (Array.isArray(param.at(0))) {
  const item: ListItem[] = param.at(0);
}

条件中调用了一次 at(0),赋值时又调用了一次。条件只检查了第一次调用的返回值,第二次调用的返回类型仍由方法签名决定:

ListItem | ListItem[] | undefined

在当前代码中,两次调用标准数组方法通常会得到同一个元素,但 TypeScript 不会因此认定两次调用的返回值必然相同。一般的函数或方法调用可能有副作用,也可能返回不同的值;调用表达式的文字相同,类型收窄机制也不会复用前一次的检查结果。

这个现象与是否使用 ReadonlyArray 无关,也不能仅用返回类型多了 undefined 来解释:第二次调用的返回值没有经过类型检查。

推荐写法:保存返回值后再判断

type ListItem = {
  a: string;
  b: number;
};

type Props = ListItem[] | ListItem[][];

function test(param: Props) {
  const firstItem = param.at(0);

  if (Array.isArray(firstItem)) {
    const item: ListItem[] = firstItem;
    // 在这里使用 item
  }
}

firstItem 保存了一次调用的结果。Array.isArray(firstItem) 成立后,TypeScript 可以将该变量收窄为 ListItem[]。数组为空时返回的 undefined 也会被这个判断排除。

保存并检查变量后,分支内仍应使用这个变量。重新调用 param.at(0) 得到的仍是方法声明中的返回类型。

undefined 和空数组还需要注意什么

启用 strictNullChecks 时,at(0) 的返回类型会保留 undefined,因为数组可能为空。param[0] 在运行时也可能得到 undefined,但普通数组索引访问的静态类型只有在启用 noUncheckedIndexedAccess 等相应检查时,才会体现这一风险。

原示例中的写法存在空数组问题:

if ('a' in param[0]) {
  const item: ListItem = param[0];
}

如果 param 为空,右侧就是 undefined,执行 in 会抛出异常。可以先把返回值存入局部变量,处理缺失值后再区分元素类型:

function test(param: Props) {
  const firstItem = param.at(0);

  if (firstItem === undefined) {
    return;
  }

  if (Array.isArray(firstItem)) {
    const items: ListItem[] = firstItem;
  } else {
    const item: ListItem = firstItem;
  }
}

这里的类型判断以参数已经符合 Props 为前提。如果数据来自外部输入,还需要单独验证对象字段;Array.isArray 只判断值是否为数组。

备注:内容仅供参考。