Home > other >  Is there a way to "reach into" a union nested inside an intersection?
Is there a way to "reach into" a union nested inside an intersection?

Time:07-01

Imagine that I have a type which is a union nested inside an intersection:

type UnionInsideIntersectionType = ({ foo: "foo" } | { foo: "bar" }) & {
  baz: "baz";
};

Is there a way to later extend the union portion of the type without rewriting it? In other words, can I somehow make the following from a transformation of UnionInsideIntersectionType?

type ExtendedUnionInsideIntersectionType = (
  | { foo: "foo" }
  | { foo: "bar" }
  | { foo: "qux" }
) & { baz: "baz" };

Playground

Conversely, is there a way to do this subtractively, i.e. to remove foo: "qux" from ExtendedUnionInsideIntersectionType to produce UnionInsideIntersectionType? I'm aware of Exclude, but if it can be applied to properties, I don't see how.

CodePudding user response:

This seems like an XY problem but I'll indulge it.


Is there a way to later extend the union portion of the type without rewriting it?

From a pure logic, and type system, point of view this:

(A | B) & C

Is exactly equivalent to:

(A & C) | (B & C)

And typescript knows that. Which means figuring out what part is a union and which part is an intersection gets a bit muddy after the type is declared. And all you have to work with is the result. Artifacts of how the type was assembled can be seen in the tooltips the compiler provides sometimes, but functionally the two cases above are perfectly equivalent. There is no sense of order of operations after the type has been declared. It just is what it is.

Another example:

type A = ({ a: 1 } | { a: 2 }) & { b: 3 }
type B = { a: 1, b: 3 } | { a: 2, b: 3 }

type Test1 = A extends B ? true : false // true
type Test2 = B extends A ? true : false // true

All of which means that I believe the answer to your first question is "no".


Conversely, is there a way to do this subtractively, i.e. to remove foo: "qux" from ExtendedUnionInsideIntersectionType to produce UnionInsideIntersectionType?

Exclude works great here:

type WithoutQux = Exclude<ExtendedUnionInsideIntersectionType, { foo: 'qux' }>

As an alternative, it seems like the UnionInsideIntersectionType type should be generic, which lets you inject whatever else you want in there.

type UnionInsideIntersectionType<
  T extends Record<string, unknown> = Record<string, never>
> = ({ foo: "foo" } | { foo: "bar" } | T) & { baz: "baz"; };

type ExtendedUnionInsideIntersectionType =
  UnionInsideIntersectionType<{ foo: 'qux' }>

See playground

  • Related