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

What is the best way to integrate Tawk.to into a React application while using typescript?

Having some issues integrating tawk.to into my website built with React and TypeScript. I have installed their official npm package, but encountered an error message: import TawkMessengerReact from '@tawk.to/tawk-messenger-react'; Could not fin ...

Steps to add annotations to a class descriptor:

Can you help me with the correct way to annotate this piece of code? export class TestCls { static SomeStaticFn(): TestCls { // Do some stuff... // Return the class descriptor for a "fluid usage" of SomeStaticFn return TestCls ...

Having trouble installing npm package in Angular project

After cloning my project from GitLab, I encountered an issue while trying to install the NPM packages. When I ran npm install, an error popped up: https://i.stack.imgur.com/WNT5s.png Upon checking the log file, I found the following error message: 3060 ...

How Typescript Omit/Pick erases Symbols in a unique way

Recently, I have delved into TypeScript and started working on developing some custom utilities for my personal projects. However, I encountered an issue with type mapping involving Pick/Omit/Exclude and other typing operations where fields with symbol key ...

Tips for neatly wrapping a class constructor

Currently, I am experimenting with code to create a more streamlined Angular Dialog initializer. This initializer should be passed a constructor function along with its arguments in a type-safe manner. The current implementation works, but it is challengi ...

How can I adjust the column width in OfficeGen?

Currently, I am utilizing officeGen for the purpose of generating word documents. <sup> let table = [ [ { val: "TT", fontFamily: "Times New Roman", }, { val: "Ten hang", ...

Leveraging Renderer in Angular 4

Understanding the importance of using a renderer instead of directly manipulating the DOM in Angular2 projects, I have gone through multiple uninstallations, cache clearings, and re-installations of Node, Typescript, and Angular-CLI. Despite these efforts, ...

Import JSON data into Angular 2 Component

After much effort, I have finally figured out how to load JSON data into an Angular 2 Component. datoer.service.ts: import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from ...

Ways to switch up the titles on UploadThing

Recently, I started working with the UploadThing library and encountered a situation where I needed to personalize some names within the code. Here is what I have so far: Below is the snippet of code that I am currently using: "use client"; imp ...

Here's a revised version: "How to link a lambda layer with a function in a serverless.ts file using the

When working with the serverless framework using the typescript template, a serverless.ts file is generated. I am currently integrating lambda layers with existing functions and encountering a typescript error. The error message reads: "Type '{ Ref: ...

Unable to make a reference to this in TypeScript

In my Angular2 application, I have a file upload feature that sends files as byte arrays to a web service. To create the byte array, I am using a FileReader with an onload event. However, I am encountering an issue where I cannot reference my uploadService ...

Mozilla struggles to interpret JSON data

When using Angular2 (RC4), I utilize this code snippet to retrieve data from my WebApi: getAppointment(id: number): Observable<Event> { return this._http.get(this._serviceUrl + 'get/' + id) .map(this.extractData) .catch ...

Asynchronous and nested onSnapshot function in Firestore with await and async functionality

I'm facing an issue with the onSnapshot method. It seems to not await for the second onsnapshot call, resulting in an incorrect returned value. The users fetched in the second onsnapshot call are displayed later in the console log after the value has ...

The React component fails to load due to the discrepancies in the data retrieved from various asynchronous requests

Creating a travel-related form using React with TypeScript. The initial component TravelForm utilizes multiple async-await requests within useEffect hook to update the state of the subsequent component TravelGuideFields However, the values of props a ...

Finding the current URL in React Router can be achieved by using specific methods and properties provided by

Currently, I'm diving into the world of react-redux with react-router. On one of my pages, it's crucial to know which page the user clicked on to be directed to this new page. Is there a method within react-router that allows me to access inform ...

How can we effectively utilize LESS variables in styles.less when working with LESS files within components in Angular versions 6 or 7?

Running Angular version 7.0.0 will generate a folder structure typical for "ng new". Below is the content of my styles.less file: @personal-black: #0000; This snippet shows the content of my app.component.less file: ...

"Transforming a callback function to an asynchronous function results in an error

I have a piece of code that is functioning as expected: var smtpConfig = { host: 'localhost', port: 465, secure: true, // use SSL selfSigned: true }; // create reusable transporter object using the default SMTP ...

Customizable TypeScript interface with built-in default key value types that can be easily extended

One common pattern that arises frequently in my projects involves fetching values and updating the UI based on the 'requestStatus' and other associated values. type RequestStatus = | 'pending' | 'requesting' | 'succ ...

Enable users to designate custom methods as either asynchronous or synchronous

These are my TypeScript method signatures: onPinnedError?(info: HavenInfo, req: Request, res: Response): HookReturnType; async onPinnedError?(info: HavenInfo, req: Request, res: Response): HookReturnType; onPinnedUnhandledRejection?(info: HavenInfo, ...

Error in pagination when using MAX() function in PostgreSQL query

Here is the query I am using to retrieve the latest message from each room: SELECT MAX ( "Messages"."id" ) AS messageId, "Rooms"."id" FROM "RoomUsers" INNER JOIN "Rooms" ON " ...