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

TypeScript联合类型转交叉类型时保留联合字段的方案咨询

Great question! Let’s break down your code, assess how well it works, and look at edge cases where it might fall short—plus how to fix them.

How Your Current Code Works

First, let’s walk through what each part of your setup does:

  • UnionToIntersection<U>: This is the classic distributive conditional trick from the post you referenced—it converts unions to intersections by leveraging TypeScript’s conditional type distribution over unions.
  • UnionToInterSectionWoNever<T>: Your custom utility tries to fix field loss by checking if a property from the intersection resolves to never; if so, it falls back to the original union’s version of that property.
  • UnionDistribution<T>: Builds a discriminated union where each member has a unionType discriminator plus type-specific properties (like aProp1 for 'A' and bProp1 for 'B').
  • The key win: ABInterSectionWoNever keeps unionType as 'A' | 'B' instead of collapsing it to never, which solves your initial field loss problem.

Reliability of Your Approach

For your specific use case—a discriminated union with a shared discriminator field—this works perfectly. Here’s why:

  • The standard UnionToIntersection collapses unionType to never because 'A' & 'B' is mutually exclusive (literals can’t be both at once).
  • Your UnionToInterSectionWoNever correctly spots this never value and replaces it with the original union’s unionType (which is 'A' | 'B').

That said, this approach has limitations when you move beyond this narrow scenario.

Potential Failure Scenarios

Let’s look at cases where your utility might not behave as expected:

1. Legitimate never Fields Get Overwritten

Suppose your union has a field that’s intentionally never for some members:

type ExtendedUnion = 
  | { unionType: 'A', optionalProp: string }
  | { unionType: 'B', optionalProp: never };

type ExtendedFix = UnionToInterSectionWoNever<ExtendedUnion>;
// Results in: { unionType: 'A' | 'B'; optionalProp: string; }

Here, the optionalProp for 'B' is supposed to be never (meaning it shouldn’t exist), but your utility merges it to string (since string | never simplifies to string), erasing that intentional constraint.

2. Nested Union Properties Collapse to never

If you have nested unions in your properties, your utility can’t recover the original nested structure:

type NestedUnion = 
  | { unionType: 'A', nested: { x: string } }
  | { unionType: 'B', nested: { y: never } };

type NestedFix = UnionToInterSectionWoNever<NestedUnion>;
// Results in: { unionType: 'A' | 'B'; nested: never; }

The nested property becomes never in the intersection, and your utility can’t bring back the original nested union ({ x: string } | { y: never }).

3. Unions Without Shared Fields (Edge Case)

While this might work for simple unshared unions, if any field resolves to never in one member, you’ll hit the same problem as Scenario 1. For example:

type UnsharedUnion = { a: string } | { b: never };
type UnsharedFix = UnionToInterSectionWoNever<UnsharedUnion>;
// Results in: { a: string; b: string; } → Wrong, since 'b' should be never for the second union member.

Better Solutions for Your Use Case

Depending on what you need to achieve, here are more robust alternatives:

Fix for Discriminated Unions (Your Original Goal)

If you only need to preserve the discriminator field while keeping the intersection of other properties, explicitly extract and re-add the discriminator instead of using a generic never check:

type ExtractDiscriminator<T extends { unionType: string }, K extends keyof T = 'unionType'> = 
  { [Key in K]: T[Key] };

type UnionToIntersectionWithDiscriminator<T extends { unionType: string }> = 
  ExtractDiscriminator<T> & Omit<UnionToIntersection<T>, 'unionType'>;

// For your original case:
type ABFixed = UnionToIntersectionWithDiscriminator<ABUnion>;
// Results in: { unionType: 'A' | 'B'; aProp1: string; aProp2: number; bProp1: string; }

This is cleaner and more reliable for your specific scenario because it directly targets the discriminator instead of guessing which fields are never accidentally.

Fix for Preserving All Original Union Properties

If you want to retain the original union’s property types (including legitimate nevers) while combining shared properties, you can use a mapped type that preserves the union for each field:

type UnionToIntersectionPreserveAll<T> = 
  { [K in keyof T]: T[K] } & UnionToIntersection<T>;

// For the extended union scenario:
type ExtendedFull = UnionToIntersectionPreserveAll<ExtendedUnion>;
// Results in: { unionType: 'A' | 'B'; optionalProp: string | never; }

If you need to explicitly retain never values for specific members, you can use a distributive type to merge each union member with the full intersection:

type UnionToIntersectionDistributed<T> = T extends any ? T & Omit<UnionToIntersection<T>, keyof T> : never;
type ExtendedCorrect = UnionToIntersectionDistributed<ExtendedUnion>;
// Results in: 
// ({ unionType: 'A'; optionalProp: string; } & { bProp1?: never; }) | 
// ({ unionType: 'B'; optionalProp: never; } & { aProp1?: never; aProp2?: never; })

This keeps the intentional never constraints while combining shared properties.

Final Verdict

Your code works great for discriminated unions where the only never field is the discriminator. But it fails when:

  • There are intentional never fields you want to preserve.
  • You have nested union properties that collapse to never.

For your original use case, the explicit discriminator extraction approach is the most robust and maintainable solution. For more complex scenarios, tailor the utility to your specific needs—like preserving nested unions or handling intentional nevers.

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 16:32:47