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 tonever; if so, it falls back to the original union’s version of that property.UnionDistribution<T>: Builds a discriminated union where each member has aunionTypediscriminator plus type-specific properties (likeaProp1for 'A' andbProp1for 'B').- The key win:
ABInterSectionWoNeverkeepsunionTypeas'A' | 'B'instead of collapsing it tonever, 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
UnionToIntersectioncollapsesunionTypetoneverbecause'A' & 'B'is mutually exclusive (literals can’t be both at once). - Your
UnionToInterSectionWoNevercorrectly spots thisnevervalue and replaces it with the original union’sunionType(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
neverfields 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

