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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 02:57:32