There is some awareness among people about conditional types distributing over unions, but what is lesser known is that mapped types also exhibit distributive behavior in specific scenarios.
Directly utilizing Nodes
does not result in distribution. For example:
type foo3 = {
[P in keyof Nodes]: 'foo'
}
// foo3 is equivalent to
// type foo3 = {
// type: "foo";
// flag: "foo";
}
This happens because keyof Nodes
only captures the common properties of the union, leading to a type with shared properties from both union constituents.
When a mapped type operates on keyof T
and T
is a type parameter instantiated with a union, the distributive nature of mapped types emerges. This means the mapped type is applied individually to each element of the union, which are then united to create the final output. Essentially,
foo<Nodes> = foo<NodeA | NodeB> = foo<NodeA> | foo<NodeB>
.
This behavior is logical for many types and usually easy to grasp. Mapped types like Readonly<T>
demonstrate distributiveness; when applied to a union, it results in a readonly union rather than just the overlapping properties.
If you wish to make a simple type distributive, you can introduce a type parameter through a conditional type:
// Distributive
type foo3 = Nodes extends infer T ? {
// ^?
[P in keyof T]: 'foo'
}: never
Alternatively, to avoid distribution, you can map over something other than keyof T
:
// Non-distributive
type foo<T> = {
[P in Exclude<keyof T, never>]: 'foo'
}
Playground Link