TypeScript中如何合法将OnlyFoo & Bar赋值给Foo & Bar?
Foo to Safely Merge Omit<Foo, keyof Bar> and Bar into Foo & Bar Let's break down your problem first using the subtraction analogy you mentioned: you're treating OnlyFoo = Omit<Foo, keyof Bar> as the "difference" between the "minuend" Foo and "subtrahend" Bar. Then you want to merge this difference back with Bar to get Foo & Bar, but TypeScript is throwing an error because Foo could be instantiated with a subtype that breaks the assignment.
Why the Error Happens
Let's use a concrete example to see what problematic subtype TypeScript is worried about. Suppose we have:
type Foo = { x: "a" }; type Bar = { x: string }; type OnlyFoo = Omit<Foo, keyof Bar>; // Results in {}
If you merge an empty OnlyFoo object with a Bar like { x: "b" }, you get { x: "b" }. But Foo & Bar simplifies to { x: "a" } (since "a" & string is just "a"). Clearly, { x: "b" } can't be assigned to { x: "a" }—that's the invalid scenario TypeScript is guarding against.
The core issue is that Foo might have properties overlapping with Bar that are narrower than Bar's corresponding properties. When you merge OnlyFoo (which drops those overlapping properties) with Bar, you end up with Bar's wider type for those properties, which doesn't match the narrower type in Foo & Bar.
The Fix: Constrain Foo to Be a Supertype of Bar's Overlapping Properties
We need to add a generic constraint that ensures any properties shared between Foo and Bar are compatible—specifically, that Bar's properties can be assigned to Foo's corresponding properties (i.e., Foo's shared properties are supertypes of Bar's).
Here's how to write the constrained factory function:
function merge<Foo, Bar extends Pick<Foo, keyof Bar>>( fooOnly: Omit<Foo, keyof Bar>, bar: Bar ): Foo & Bar { return { ...fooOnly, ...bar } as Foo & Bar; }
Let's unpack the constraint Bar extends Pick<Foo, keyof Bar>:
Pick<Foo, keyof Bar>extracts all properties fromFoothat exist inBar.- The constraint says that
Barmust be assignable to this extracted type. In plain terms: every property inBarmust match (or be a subtype of) the same property inFoo.
Testing the Fix
Let's test with the problematic example from earlier:
type Foo = { x: "a" }; type Bar = { x: string }; // Now TypeScript throws an error HERE, before even calling merge! // Because Bar (x: string) doesn't extend Pick<Foo, keyof Bar> (x: "a") const bar: Bar = { x: "b" }; merge({}, bar); // Error: Type 'string' is not assignable to type '"a"'
This is good—TypeScript now catches the incompatibility at the constraint level, instead of letting an invalid assignment slip through.
For valid cases where the constraint is satisfied:
type Foo = { x: string; y: number }; type Bar = { x: "hello" }; type OnlyFoo = Omit<Foo, keyof Bar>; // { y: number } const fooOnly: OnlyFoo = { y: 42 }; const bar: Bar = { x: "hello" }; const merged = merge(fooOnly, bar); // Type is Foo & Bar = { x: "hello"; y: number } // No error—this works perfectly!
Why This Works
When Bar extends Pick<Foo, keyof Bar> holds, Foo & Bar simplifies to Omit<Foo, keyof Bar> & Bar. Because:
Foo & BarcombinesFoo's non-overlapping properties, plus the intersection ofFoo's overlapping properties andBar's overlapping properties.- Since
Bar's overlapping properties are subtypes ofFoo's, their intersection is justBar's properties. - So
Foo & Baris exactly the type you get by mergingOmit<Foo, keyof Bar>andBar, making the assignment valid.
内容的提问来源于stack exchange,提问作者totokaka

