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

TypeScript区分联合类型:限制属性与StrictUnion传参问题解决

TypeScript联合类型的两个问题解决

问题1:禁止联合类型实例包含其他分支的独有属性

原始代码

type Base = {
    base : string,
    count : number
}
type ExtendedA = Base & {
    special : string
}
type ExtendedB = Base & {
    hidden : number
}

function EXAMPLE (payload : ExtendedA | ExtendedB) {
    if ("special" in payload) {
        payload.special
    }
    else {
        payload.hidden
    }
}

问题描述

当创建类型为Parameters<typeof EXAMPLE>[0]的对象并指定hidden属性(属于ExtendedB)时,TypeScript不会阻止同时传入special属性——因为结构类型系统中,只要对象满足联合类型某一分支的所有必填项,就会被视为合法。如何确保这类对象只能属于其中一个分支,不能同时包含其他分支的独有属性?

解决方法

使用StrictUnion类型改造联合类型,将其他分支的独有属性标记为never(可选,但传入即报错):

// 提取联合类型所有分支的键
type UnionKeys<T> = T extends T ? keyof T : never;
// 为每个分支添加其他分支键的never约束
type StrictUnionHelper<T, TAll> = 
    T extends any 
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
// 生成严格联合类型
type StrictUnion<T> = StrictUnionHelper<T, T>

// 修改EXAMPLE的参数类型
function EXAMPLE (payload : StrictUnion<ExtendedA | ExtendedB>) {
    if ("special" in payload) {
        payload.special
    }
    else {
        payload.hidden
    }
}

此时如果创建同时包含hidden和special的对象,TypeScript会直接报错,因为special在ExtendedB分支的StrictUnion类型中被标记为never,不允许赋值。


问题2:StrictUnion类型下的类型守卫缩小失效问题

问题场景

使用StrictUnion改造后,尝试将payload传入对应分支的函数时出现类型错误:

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = 
    T extends any 
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

function SPECIAL (payload : ExtendedA) {}
function HIDDEN (payload : ExtendedB) {}
function EXAMPLE (payload : StrictUnion<ExtendedA | ExtendedB>) {
    if ("special" in payload) {
        SPECIAL(payload) // 类型错误:payload不能赋值给ExtendedA
    }
    else {
        HIDDEN(payload) // 类型错误:payload不能赋值给ExtendedB
    }
}

问题原因

StrictUnion生成的类型是(ExtendedA & { hidden?: never }) | (ExtendedB & { special?: never }),TypeScript的in类型守卫只能识别special属性存在,但无法自动将payload缩小到ExtendedA——因为交叉类型ExtendedA & { hidden?: never }和原始ExtendedA不是完全等价的类型。

解决方法

  • 方法1:使用类型断言
    在类型守卫后手动断言payload的类型:
function EXAMPLE (payload : StrictUnion<ExtendedA | ExtendedB>) {
    if ("special" in payload) {
        SPECIAL(payload as ExtendedA)
    }
    else {
        HIDDEN(payload as ExtendedB)
    }
}
  • 方法2:自定义类型谓词
    创建更精确的类型守卫函数,明确判断payload属于哪个分支:
function isExtendedA(payload: StrictUnion<ExtendedA | ExtendedB>): payload is ExtendedA {
    return "special" in payload;
}

function isExtendedB(payload: StrictUnion<ExtendedA | ExtendedB>): payload is ExtendedB {
    return "hidden" in payload;
}

function EXAMPLE (payload : StrictUnion<ExtendedA | ExtendedB>) {
    if (isExtendedA(payload)) {
        SPECIAL(payload) // 类型自动缩小为ExtendedA
    }
    else {
        HIDDEN(payload) // 类型自动缩小为ExtendedB
    }
}
  • 方法3:调整StrictUnion的定义
    让StrictUnion生成的类型可以被TypeScript自动识别为原始分支类型,比如去掉Partial(但这样会要求必须显式设置其他分支属性为undefined):
type StrictUnionHelper<T, TAll> = 
    T extends any 
    ? T & Record<Exclude<UnionKeys<TAll>, keyof T>, never> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

// 使用时必须显式设置其他分支属性为undefined
const validB: StrictUnion<ExtendedA | ExtendedB> = {
    base: "test",
    count: 1,
    hidden: 2,
    special: undefined // 必须显式设置为undefined
}

这种方法虽然能让类型守卫自动生效,但会增加使用成本,需要显式设置多余属性为undefined。


内容的提问来源于stack exchange,提问作者A. L

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 14:36:54