I have some types, which share common props but have different objects for those props. I'd like to union those sub-objects, but it's not working how I'd expect. This is my code:
interface IFoo { foo: any };
interface IBarFnord { bar?: any; fnord?: any };
type Union = {test: IFoo } & {test: IBarFnord; otherTest?: IBarFnord };
const fails: Union = { test: { foo: null }};
const works: Union = { test: {foo: null, bar: null }};
const alsoWorks: Union = { test: {foo: null, fnord: null }};
For const fails
I get the error:
Type '{ foo: null; }' has no properties in common with type 'IBarFnord'.
And this is true. If the Union means is must have properties in both, then it makes sense.
I've tested doing this not as a subprop, and it works fine:
type Union = {test: IFoo & IBarFnord };
const worksNow: Union = { test: { foo: null }};
const stillWorks: Union = { test: {foo: null, bar: null }};
Anyways, how can I tell Typescript I want to union these things, but I don't expect every item to always have props in both sides of the union?
Assuming you actually mean "intersection" and not "union", this looks like a known issue in which excess property checking applies to each constituent of the intersection separately, when it should really only apply to the full intersection as a whole. Looks like there has been some recent work done to address this, so maybe a fix will make it into an upcoming release? Not sure.
Anyway, as a workaround, we can make our own type alias which aggressively and recursively merges subproperties in intersection types, like this:
type SubpropertyMerge<T> = T extends (...args: infer A) => infer R
? (...args: SubpropertyMerge<A>) => SubpropertyMerge<R>
: T extends object ? { [K in keyof T]: SubpropertyMerge<T[K]> } : T;
This type should transform a type consisting of primitives, objects, arrays/tuples, and non-generic functions into an equivalent type where any intersections are merged:
type Test = {
a: string;
b: number[];
c: {
d: boolean;
e: string | { f: number };
};
g: [string, number, { h: number }];
h: () => string;
i: (x: number) => number;
j: (x: { foo: string }) => number[];
};
type MergedTest = SubpropertyMerge<Test>; // looks the same
// MutuallyExtends<A, B> is a compile error unless A and B are mutually assignable
type MutuallyExtends<T extends U, U extends V, V=T> = true;
type TestIsOkay = MutuallyExtends<Test, MergedTest>; // acts the same
And it should work on your type, which I've renamed to Intersection
:
type MergedIntersection = SubpropertyMerge<Intersection>;
type IntersectionIsOkay = MutuallyExtends<Intersection, MergedIntersection>; // acts the same
But now your assignment should behave as you expect:
const worksNow: MergedIntersection = { test: { foo: null } };
Okay, hope that helps; good luck!