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

TypeScript无法收窄非字面量类型,如何定义eat函数参数类型避免编译报错

问题描述

我写了一个简易的解析器,代码如下:

const str = "String to test";
let pos = 0;
function eat(
  ...args: (
    | [RegExp, (result: RegExpExecArray) => boolean]
    | [string, (result: string) => boolean]
    | [string[], (result: number) => boolean]
  )[]
) {
  const oldPos = pos;
  for (const [test, callback] of args) {
    if (test instanceof RegExp) {
      test.lastIndex = pos;
      const match = test.exec(str);
      if (match) {
        pos = test.lastIndex;
        if (callback(match)) return true;
        break;
      }
    } else if (typeof test === "string") {
      if (str.substr(pos, test.length) === test) {
        pos += test.length;
        if (callback(test)) return true;
        break;
      }
    } else {
      const temp = test.findIndex(item => str.substr(pos, item.length) === item);
      if (temp != -1) {
        pos += test[temp].length;
        if (callback(temp)) return true;
        break;
      }
    }
  }
  pos = oldPos;
  return false;
}
if (eat(
  [/string/iy, result /* RegExpExecArray */ => {
    // ...
    return true;
  }],
  [["foo", "bar"], result /* number */ => {
    // ...
    return true;
  }]
)) console.log("Matched");

我找不到正确定义args参数类型的方法,无法让编译器正常不报错。
我知道可以把传给callback的match、test、temp(即result参数)用as never做强制类型转换,但这样在调用方法的时候也无法正确推断result的类型。
另一种可行思路是像下方示例一样添加字符串标签:

function example(arg: ["string", string, number] | ["boolean", boolean, bigint]) {
    if (arg[0] === "string") {
        Math.floor(arg[2]); // compiler happy
    }
}

但这种方案在调用时会非常繁琐,而且在我的业务场景下也无法生效。
我也尝试过使用泛型和函数重载,要么不生效,要么无法满足我的需求——我调用该函数时会混用三种类型的参数数组。
请问大家有什么解决思路?这是不是TypeScript的bug,我是否需要到GitHub的issues页面提交这个问题?


解决方案

这个不是TypeScript的bug,属于当前TS对联合类型元组的关联项收窄能力的现有限制:你判断了元组第一个元素test的类型,但是TS不会自动关联推导同一个元组里第二个元素callback的参数类型。

你可以用自定义类型谓词封装收窄逻辑的方案解决,不需要改调用方式,也能保留完整的类型推断:

  1. 先定义三种参数元组的类型别名,再写自定义类型守卫函数,用来收窄每一种元组类型
type RegExpRule = [RegExp, (result: RegExpExecArray) => boolean]
type StringRule = [string, (result: string) => boolean]
type StringArrayRule = [string[], (result: number) => boolean]
type EatRule = RegExpRule | StringRule | StringArrayRule

function isRegExpRule(rule: EatRule): rule is RegExpRule {
  return rule[0] instanceof RegExp
}
function isStringRule(rule: EatRule): rule is StringRule {
  return typeof rule[0] === 'string'
}
function isStringArrayRule(rule: EatRule): rule is StringArrayRule {
  return Array.isArray(rule[0])
}
  1. 改写eat函数里的循环逻辑,遍历每个rule的时候用自定义守卫收窄类型
function eat(...args: EatRule[]) {
  const oldPos = pos;
  for (const rule of args) {
    if (isRegExpRule(rule)) {
      const [test, callback] = rule
      test.lastIndex = pos;
      const match = test.exec(str);
      if (match) {
        pos = test.lastIndex;
        if (callback(match)) return true;
        break;
      }
    } else if (isStringRule(rule)) {
      const [test, callback] = rule
      if (str.substr(pos, test.length) === test) {
        pos += test.length;
        if (callback(test)) return true;
        break;
      }
    } else {
      const [test, callback] = rule
      const temp = test.findIndex(item => str.substr(pos, item.length) === item);
      if (temp != -1) {
        pos += test[temp].length;
        if (callback(temp)) return true;
        break;
      }
    }
  }
  pos = oldPos;
  return false;
}

改完之后编译器不会报错,调用eat的时候传入的回调参数也会自动推断正确类型,不需要加任何额外标记,完全符合你的使用需求。

如果你觉得写三个类型守卫太麻烦,也可以在确定类型匹配的地方用as unknown as 对应类型临时断言,但是自定义类型守卫是可维护性最好、也最安全的方案。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 13:54:03