TS 5.4.4中如何省略永远为false分支的返回类型?
问题
我当前使用TypeScript 5.4.4,遇到如下类型处理需求:
简化后的场景代码:
const isNumber = (value:unknown): value is number => typeof value === 'number' const isString = (value:unknown): value is string => typeof value === 'string' const doSomethingWithString = (str: string) => 'This is what should happen to a string' as const const doSomethingWithNumber = (str: number) => 'Is this really a number?' as const const doSomething = <T extends string|number>(arg:T) => { if(isNumber(arg)) { return doSomethingWithNumber(arg) } if(isString(arg)) { return doSomethingWithString(arg) } throw new Error('Unreachable part of the code reached!') } const test = doSomething('string') const testNum = doSomething(1)
当前test和testNum的类型均为"Is this really a number?" | "This is what should happen to a string",但我期望传入string类型参数时,返回类型仅为"This is what should happen to a string";传入number时仅返回对应数字处理后的类型。
经研究发现,TypeScript不会省略值为never的分支的返回类型,但会省略返回值本身为never的情况:
const isNumber = (value:unknown): value is number => typeof value === 'number' const doSomething = (a:string)=>{ if(isNumber(a)) { // TypeScript能识别a是never,但仍会把这个分支的返回纳入类型 return `This is never: ${a}` } return 'This is the correct return' as const } const doSomethingElse = (a:string)=>{ if(isNumber(a)) { // 返回值本身是never,所以不会被纳入类型,函数返回类型正确 return a } return 'This is the correct return' as const }
我已经实现了一个基于NeverByPredicate泛型的解决方案,但存在代码重复和维护隐患(修改if分支谓词但未同步修改泛型参数会出错):
const isNumber = (value:unknown): value is number => typeof value === 'number' const isString = (value:unknown): value is string => typeof value === 'string' const doSomethingWithString = (str: string) => 'This is what should happen to a string' as const const doSomethingWithNumber = (str: number) => 'Is this really a number?' as const type NeverByPredicate<P, T> = P extends never ? never : T; const doSomething = <T extends string|number>(arg:T) => { if(isNumber(arg)) { const res = doSomethingWithNumber(arg) return res as NeverByPredicate<typeof arg, typeof res> } if(isString(arg)) { const res = doSomethingWithString(arg) return res as NeverByPredicate<typeof arg, typeof res> } throw new Error('Unreachable part of the code reached!') } const test = doSomething('string') const testNum = doSomething(1)
请问是否存在更优雅的解决方案?
更优雅的解决方案
方案1:使用函数重载
直接为doSomething定义重载签名,明确不同入参对应的返回类型,实现逻辑保持不变:
const isNumber = (value:unknown): value is number => typeof value === 'number' const isString = (value:unknown): value is string => typeof value === 'string' const doSomethingWithString = (str: string) => 'This is what should happen to a string' as const const doSomethingWithNumber = (str: number) => 'Is this really a number?' as const // 重载签名 function doSomething(arg: string): typeof doSomethingWithString; function doSomething(arg: number): typeof doSomethingWithNumber; // 实现签名 function doSomething(arg: string | number) { if(isNumber(arg)) { return doSomethingWithNumber(arg) } if(isString(arg)) { return doSomethingWithString(arg) } throw new Error('Unreachable part of the code reached!') } const test = doSomething('string') // 类型:"This is what should happen to a string" const testNum = doSomething(1) // 类型:"Is this really a number?"
这种方式最直观,直接通过重载明确入参和返回值的映射关系,没有额外的类型逻辑,维护成本低。
方案2:利用条件类型推导返回值
通过为函数指定返回类型为条件类型,让TypeScript根据入参T自动推导对应的返回值:
const isNumber = (value:unknown): value is number => typeof value === 'number' const isString = (value:unknown): value is string => typeof value === 'string' const doSomethingWithString = (str: string) => 'This is what should happen to a string' as const const doSomethingWithNumber = (str: number) => 'Is this really a number?' as const const doSomething = <T extends string | number>(arg: T): T extends number ? typeof doSomethingWithNumber : typeof doSomethingWithString => { if(isNumber(arg)) { return doSomethingWithNumber(arg) as any } if(isString(arg)) { return doSomethingWithString(arg) as any } throw new Error('Unreachable part of the code reached!') } const test = doSomething('string') // 类型:"This is what should happen to a string" const testNum = doSomething(1) // 类型:"Is this really a number?"
这里通过条件类型T extends number ? ... : ...直接关联入参和返回值,只需要一次类型定义,避免了重复的类型断言。
方案3:封装断言工具函数
如果你不想用重载或条件类型,也可以封装一个通用的断言工具,消除重复的NeverByPredicate使用:
const isNumber = (value:unknown): value is number => typeof value === 'number' const isString = (value:unknown): value is string => typeof value === 'string' const doSomethingWithString = (str: string) => 'This is what should happen to a string' as const const doSomethingWithNumber = (str: number) => 'Is this really a number?' as const type NeverByPredicate<P, T> = P extends never ? never : T; const assertNeverBranch = <P, T>(predicateValue: P, result: T): NeverByPredicate<P, T> => result as any const doSomething = <T extends string|number>(arg:T) => { if(isNumber(arg)) { return assertNeverBranch(arg, doSomethingWithNumber(arg)) } if(isString(arg)) { return assertNeverBranch(arg, doSomethingWithString(arg)) } throw new Error('Unreachable part of the code reached!') } const test = doSomething('string') // 类型:"This is what should happen to a string" const testNum = doSomething(1) // 类型:"Is this really a number?"
通过封装assertNeverBranch工具函数,把重复的类型断言逻辑抽离出来,避免了代码重复,也降低了维护时的同步错误风险。
内容的提问来源于stack exchange,提问作者Michał Sadowski

