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的参数类型。
你可以用自定义类型谓词封装收窄逻辑的方案解决,不需要改调用方式,也能保留完整的类型推断:
- 先定义三种参数元组的类型别名,再写自定义类型守卫函数,用来收窄每一种元组类型
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]) }
- 改写
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
相关产品推荐
相关产品推荐

