Creating an object type that accommodates the properties of all union type objects, while the values are intersections, involves a unique approach

How can I create a unified object type from multiple object unions, containing all properties but with intersecting values?

For example, I want to transform the type

{ foo: 1 } | { foo: 2; bar: 3 } | { foo: 7; bar: 8 }
into the type {foo: 1 | 2 | 7; bar: 3 | 8}.

Note: Instead of creating an intersection like {foo: 1 | 2} & {bar: 3}, I aim to generate a single object type.

I've developed a type called ComplexUnionToIntersection to achieve this. However, it currently disregards properties that don't exist in all objects within the union (such as `bar` in my examples).

Here is the code snippet:

/**
 * More info: https://fettblog.eu/typescript-union-to-intersection/
 */
export type UnionToIntersection<U> = (
    U extends any ? (k: U) => void : never
) extends (k: infer I) => void
    ? I
    : never;

/**
 * Target type
 */
export type ComplexUnionToIntersection<U> = { o: U } extends { o: infer X }
    ? {
            [K in keyof (X & U)]: (X & U)[K];
      }
    : UnionToIntersection<U>;

Test cases:

// TODO: test case should result in `{foo: 1 | 2; bar: 3}`
type testCase1 = ComplexUnionToIntersection<{ foo: 1 } | { foo: 2; bar: 3 }>; // currently returns `{ foo: 1 | 2; }`

// TODO: test case should result in `{foo: 1 | 2 | 7; bar: 3 | 8}`
type testCase2 = ComplexUnionToIntersection<
    { foo: 1 } | { foo: 2; bar: 3 } | { foo: 7; bar: 8 }
>;

// TODO: test case should result in `{foo: 1 | 2; bar: 3 | 8}`
type testCase3 = ComplexUnionToIntersection<
    { foo: 1 } | { foo: 2; bar: 3 } | { bar: 8 }
>;

// TODO: test case should result in `{foo?: 1 | 2; bar: 3 | 8}`
type testCase4 = ComplexUnionToIntersection<
    { foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }
>;

Access TS Playground here

Answer №1

In order to consolidate a variety of object types into a single object type, where each property key from any input union member will be present in the resulting type along with the combined value type for that key across all input members, including optional properties if they exist in any input.

If we simply merge the union into a single object type directly, we face the issue where a property will only show up in the output if it appears in every input, while maintaining correct property types. One solution is to enhance each union member by adding all keys present in any member that might be missing, assigning them the 'never' type which gets absorbed in any union.

For instance, starting with:

{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }

We then augment each union member to include all keys like:

{ foo: 1; bar: never } | { foo?: 2; bar: 3 } | { foo: never; bar: 8 }

Next step is to merge this augmented union into a single object like:

{ foo?: 1 | 2 | never; bar: never | 3 | 8 }

which simplifies to

{ foo?: 1 | 2; bar: 3 | 8 }

Let's implement this:


type AllProperties<T> = T extends unknown ? keyof T : never

type AddMissingProperties<T, K extends PropertyKey = AllProperties<T>> =
    T extends unknown ? (T & Record<Exclude<K, keyof T>, never>) : never;

type MergeObjects<T> = { [K in keyof AddMissingProperties<T>]: AddMissingProperties<T>[K] }

The AllProperties<T> type captures all keys from union members using a distributive conditional type:

type TestAllProperties = AllProperties<{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }>
// type TestAllProperties = "foo" | "bar"

The AddMissingProperties<T, K> type follows the same pattern, by incorporating any absent keys from K into each union element and assigning them the 'never' type, defaulting K to AllProperties<T>:

type TestAddMissingProperties = AddMissingProperties<{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }>
/* type TestAddMissingProperties = 
    ({ foo: 1; } & Record<"bar", never>) | 
    ({ foo?: 2 | undefined; bar: 3; } & Record<never, never>) | 
    ({ bar: 8; } & Record<"foo", never>) */

This gives the equivalent result as mentioned above, albeit in a different format. The structure doesn't impact further processing of the type.

Lastly, the MergeObjects<T> type acts as an identity mapped type over AddMissingProperties<T>, iterating through each property to generate a unified object output:

type TestMergeObjects = MergeObjects<{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }>
/* type TestMergeObjects = {
    foo?: 1 | 2 | undefined;
    bar: 3 | 8;
} */

Everything seems to be in order!

Explore the code on TypeScript Playground

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is it possible to begin the vue root instance by using a .vue class component?

Utilizing vue-class-component allows me to incorporate class syntax and TypeScript type checking within my .vue files. I can easily create .vue files and register them as components using this method, with the exception of the root Vue() instance. This ap ...

Issue encountered with the inability to successfully subscribe to the LoggedIn Observable

After successfully logging in using a service in Angular, I am encountering an error while trying to hide the signin and signup links. The error message can be seen in this screenshot: https://i.stack.imgur.com/WcRYm.png Below is my service code snippet: ...

Using the Vue.js Compositions API to handle multiple API requests with a promise when the component is mounted

I have a task that requires me to make requests to 4 different places in the onmounted function using the composition api. I want to send these requests simultaneously with promises for better performance. Can anyone guide me on how to achieve this effic ...

Develop a variety of Jabber client echo bots

Hello Stackoverflow Community, I've been trying different approaches to resolve my issue, but I keep ending up with stack overflow errors. Programming Language: Typescript Main Objective: To create multiple instances of the Client Class that can be ...

Utilizing ngFor to iterate over items within an Observable array serving as unique identifiers

Just starting out with Angular and I'm really impressed with its power so far. I'm using the angularfire2 library to fetch two separate lists from firebase (*.ts): this.list1= this.db.list("list1").valueChanges(); this.list2= this.db.list("list2 ...

Retrieving child elements from parent identifiers using Typescript

I've been working on creating a new array with children from the data fetched from my database. While my initial attempt was somewhat successful, I believe there are some missing pieces. Could you assist me with this? Here is the raw data retrieved f ...

What is the best way to incorporate the C# in keyword into TypeScript?

When coding in C#, I often utilize the in keyword within conditional statements. if (1.In(1, 2) I am curious about how to implement the IN keyword in typescript. ...

Is there a way for me to simultaneously run a typescript watch and start the server?

While working on my project in nodejs, I encountered an issue when trying to code and test APIs. It required running two separate consoles - one for executing typescript watch and another for running the server. Feeling frustrated by this process, I came ...

The cause of Interface A improperly extending Interface B errors in Typescript

Why does extending an interface by adding more properties make it non-assignable to a function accepting the base interface type? Shouldn't the overriding interface always have the properties that the function expects from the Base interface type? Th ...

Unspecified parameter for Next.js dynamic route

Currently, I am developing an e-commerce application using next.js with Typescript and MongoDB. To better understand my project, let's take a look at my existing file structure: https://i.stack.imgur.com/tZqVm.png The mainPage.tsx file is responsibl ...

What is the process for turning off deep imports in Tslint or tsconfig?

Is there a way to prevent deep imports in tsconfig? I am looking to limit imports beyond the library path: import { * } from '@geo/map-lib'; Despite my attempts, imports like @geo/map-lib/src/... are still allowed. { "extends": &q ...

Can one mimic a TypeScript decorator?

Assuming I have a method decorator like this: class Service { @LogCall() doSomething() { return 1 + 1; } } Is there a way to simulate mocking the @LogCall decorator in unit tests, either by preventing it from being applied or by a ...

How can I detect if a control value has been changed in a FormGroup within Angular 2? Are there any specific properties to look

I am working with a FormGroup that contains 15 editable items, including textboxes and dropdowns. I am looking to identify whether the user has made any edits to these items. Is there a specific property or method I can use to check if the value of any i ...

A Typescript Function for Generating Scalable and Unique Identifiers

How can a unique ID be generated to reduce the likelihood of overlap? for(let i = 0; i < <Arbitrary Limit>; i++) generateID(); There are several existing solutions, but they all seem like indirect ways to address this issue. Potential Solu ...

How can an array be generated functionally using properties from an array of objects?

Here's the current implementation that is functioning as expected: let newList: any[] = []; for (let stuff of this.Stuff) { newList = newList.concat(stuff.food); } The "Stuff" array consists of objects where each ...

Step-by-step guide to initializing data within a service during bootstrap in Angular2 version RC4

In this scenario, I have two services injected and I need to ensure that some data, like a base URL, is passed to the first service so that all subsequent services can access it. Below is my root component: export class AppCmp { constructor (private h ...

Issue: Unable to link with 'dataSource' as it is not a recognized feature of 'mat-tree'

Upon following the example provided at https://material.angular.io/components/tree/overview, I encountered an error when trying to implement it as described. The specific error message is: Can't bind to 'dataSource' since it isn't a kn ...

Setting up grunt-ts to function seamlessly with LiveReload

I am currently experimenting with using TypeScript within a Yeoman and Grunt setup. I've been utilizing a Grunt plugin called grunt-ts to compile my TypeScript files, and while the compilation process is smooth, I'm encountering issues with live ...

What could be the reason for TypeScript inferring the generic type as an empty string?

I have developed a React component known as StateWithValidation. import { useStateWithValidation } from "./useStateWithValidation"; export const StateWithValidation = () => { const [username, setUserName, isValid] = useStateWithValidation( ( ...

Transforming a JavaScript chained setter into TypeScript

I have been utilizing this idiom in JavaScript to facilitate the creation of chained setters. function bar() { let p = 0; function f() { } f.prop = function(d) { return !arguments.length ? p : (p = d, f); } return f; } ...