Deriving types from object combinations

Can the Foo type be 'flattened' to return { A?: string; B? number } in the code snippet below?

type Foo = { A: string } | { B: number }

type Flatten<
  T,
  Keys extends keyof T = T extends any ? keyof T : never,
> = { [K in Keys]?: T[K] }


type Bar = Flatten<Foo> 
// { A?: unknown; B? unknown }, but can it be changed to `{ A?: string; B? number }`?

code

Answer №1

My interpretation of the Flatten<T> operation is like condensing a union of object types into a single object type, assuming that object types were sealed or "exact" (as mentioned in microsoft/TypeScript#12936) and did not allow extra properties.

Presently, object types are open or extensible, and they permit excess properties. For instance, a value of type {a: string} may have a string-valued property a, along with other properties such as b. The absence of b in the type definition does not mean it's missing. The compiler considers the property at key b in {a: string} to be of type unknown, because it could be anything. Therefore, the b property in {a: string} | {b: number} is also of type unknown. This results in flattening to something like {a?: unknown, b?: unknown}, which is not ideal.

There isn't currently a way to specify that all unspecified keys should be prohibited. You can make use of

Exact<{a: string}> | Exact<{b: number}>
but this does not completely achieve what you desire. If a particular key must be forbidden, the closest solution is to define it as an optional property with a value of never.

The plan here involves collecting all keys from each union member of T:

  1. Determine AllKeys<T> - keys from all union members of T.
  2. Restrict each union member of T by prohibiting unmentioned keys from AllKeys<T>.
  3. Combine the new union members into a single object type.

For example, if T is {a: string} | {b: number}:

  1. AllKeys<T> would be "a" | "b".
  2. Restrictions on unmentioned keys for each member:
    • From {a: string} to {a: string; b?: never} to restrict b.
    • From {b: number} to {a?: never; b: number} to restrict a.
  3. Merge
    {a: string; b?: never} | {a?: never; b: number}
    into a single object type {a?: string; b?: number}.

This explains the implementation approach.


Let's begin with implementing AllKeys<T>:

type AllKeys<T> = T extends unknown ? keyof T : never;

This straightforward conditional type gathers keys from each member of T together.

We then move on to creating ProhibitExtra<T, K>which prohibits any extra keys from K in an object type T:

type ProhibitExtra<T, K extends PropertyKey> =
  T & { [P in Exclude<K, keyof T>]?: never };

This operation includes optional never-typed properties in T for every unmentioned key in

K</code.</p>
<p>Next, we develop <code>ExactifyUnion<T>
to apply ProhibitExtra<T, K> to unions in T, where K is AllKeys<T> for the original T:

type ExactifyUnion<T, K extends PropertyKey = AllKeys<T>> =
  T extends unknown ? ProhibitExtra<T, K> : never

Finally, we create Flatten<T> by utilizing ExactifyUnion<T> to obtain a new union and collapsing it into a single object type:

type Flatten<T> = Omit<ExactifyUnion<T>, never>;

The above method leverages the fact that the Omit utility type does not distribute over unions, making it an easy way to create a non-distributed mapped type.


Let's test our implementation:

type F1 = Flatten<{ a: string } | { b: number }> 
// Type F1 = { a?: string; b?: number }

That aligns with the desired output. It's important to note that when a property is present in all union members, it should not be optional:

type F2 = Flatten<{ a: string } | { a: number }> 
// Type F2 { a: string | number }

A glimpse into handling optional/required properties across different union members:

type F3 = Flatten<
  { a?: string, b: number, c: boolean, d?: Date } |
  { c: string, d?: number, e: boolean, f?: Date }>
/* Type F3 = {
    a?: string;
    b?: number;
    c: string | boolean;
    d?: number | Date;
    e?: boolean;
    f?: Date;
} */

The results look promising!

Check out the Playground link for the code

Answer №2

Here's a simple solution that works nicely:

interface Animal {
  name: string;
  age: number;
}

interface Food {
  type: string;
  quantity: number;
}

type Zoo = Animal | Food;

const myZoo: Zoo = {name: 'Leo', age: 3};

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

Discovering if a page can be scrolled in Angular

Hey there, I recently started working with Angular and created an app using the angular material stepper. The issue I'm facing is that some of the steps have longer content, causing the page to become scrollable. I am now trying to find a way to deter ...

The parameter type '(value: any) => any[]' does not match the expected parameter type 'string[]'

I'm encountering an issue where I can't push a value to a state array due to a TS error. Let me highlight the relevant components where the error occurs. The error specifically lies in the child component, within the handleValue(value => [...v ...

NGRX reducer avoids generating errors due to incorrect assignments

My experience with ngrx is relatively new. In my typical TypeScript work, I usually encounter an incorrect assignment error like the one below due to a missing property in the interface declaration: interface IExample { count: number; } let initialState ...

Angular 2: A guide to resetting dropdown and text values when changing radio button selections

When the user interface displays two radio buttons - one for YES and one for NO - and the user clicks on YES, a dropdown is shown. Conversely, if the user clicks on NO, a textbox is displayed. How can I clear the values in the dropdown and textbox when s ...

What is the best way to retrieve the current complete URL in a Next.js/Typescript component?

I'm working on a component and I need to retrieve the current full URL. Here's a simplified version of what I have: /** * Share dropdown component */ export const ShareDropdown: React.FC<{ className: string }> = ({ className, }) => { ...

Incorporating a Component with lazy-loading capabilities into the HTML of another Component in Angular 2+

Striving to incorporate lazy loading in Angular 2, I have successfully implemented lazy loading by following this helpful guide. Within my application, I have two components - home1 and home2. Home1 showcases the top news section, while home2 is dedicated ...

Creating a component in Angular that utilizes multiple nested FormGroups

When attempting to nest multiple FormGroups, everything works smoothly if the template is not extracted into separate components. For instance, the following example functions as expected: Template <form [formGroup]="baseForm"> <div formGr ...

Utilizing Next.js to create a Higher Order Component (HOC) for fetching data from a REST API using Typescript is proving to be a challenge, as the

In my withUser.tsx file, I have implemented a higher order component that wraps authenticated pages. This HOC ensures that only users with a specified user role have access to the intended pages. import axios, { AxiosError } from "axios"; import ...

Breaking down an array into function arguments in TypeScript/JavaScript

I am working with an array of object instances that have different types. var instances: any = []; instances["Object1"] = new TypeA(); instances["ObjectB"] = new TypeB(); Each type has its own methods with unique names and varying numbers of arguments. I ...

What is the best way to obtain a distinct collection from two arrays that eliminates the second appearance of an element based on a key's value, rather than the first as in the Lodash uniqueBy function?

Let's say I have two arrays... const arr1 = [ { id: 1: newBid: true } ]; const arr2 = [ { id: 1, newBid: false }, { id: 2, newBid: false } ]; My goal is to end up with an array that looks like this [ { id: 1, newBid: false }, { id: 2, newBid: fals ...

Issue "Value of type '{}' cannot be assigned to parameter of type 'T | (() => T)'" encountered within a React component containing a type parameter

Currently, I am attempting to achieve the following: function SomeComponent<T>({ children }: PropsType) { const [configuration, setConfiguration] = useState<T>({}) } However, I am encountering this issue: The argument of type '{}&apos ...

Combine an array of arrays with its elements reversed within the same array

I am working with an array of numbers that is structured like this: const arrayOfArrays: number[][] = [[1, 2], [1, 3]]; The desired outcome is to have [[1, 2], [2, 1], [1, 3], [3, 1]]. I found a solution using the following approach: // initialize an e ...

The function is missing a closing return statement and the return type does not specify 'undefined'

It seems like the function lacks an ending return statement and the return type does not include 'undefined'. In a recent refactoring of the async await function called getMarkets, I noticed that I had mistakenly set the return type as Promise: ...

Testing Vue components with Typescript and Jest does not display the expected values in the HTML output

Recently, I decided to focus on Test-Driven Development (TDD) using Typescript, so I started a new Vue project with vue-cli. I specifically chose Vue3, Typescript, and Jest for this project. However, when I ran the unit test initially, it failed to execute ...

Are you encountering issues with Google Analytics performance on your Aurelia TypeScript project?

I recently started using Google Analytics and I am looking to integrate it into a website that I'm currently building. Current scenario Initially, I added the Google Analytics tracking code to my index.ejs file. Here is how the code looks: <!DOC ...

"Encountering issues with importing Splitpanes while using VueJs and TypeScript combination

My VueJS view was originally written in JavaScript using the component "splitpanes" from npm package. The code was functioning well with the following structure: <template> <div> <Splitpanes :dbl-click-splitter="false" :horizont ...

Uncovering the origins of computed object keys in TypeScript

I am currently working on a project where I need to easily define and use new plugins using TypeScript in my IDE. My folder structure looks like this: src │ ... └── plugins └── pluginA | index.ts └── pluginB | index. ...

Linking Redux to the highest level of my application is not functioning as expected

I have been attempting to troubleshoot this code for quite some time now, but I am struggling to identify the issue at hand. My main goal is to establish a connection between my top-level application and the redux store. However, every time I try, the stor ...

Utilizing an Angular Service within the main.ts script

My main.ts file currently has the following code snippet: declare const require; const translations = require("raw-loader!./locale/messages.de.xlf"); platformBrowserDynamic().bootstrapModule(AppModule, { providers: [ { provide: TRANSLATIONS, useVa ...

Utilizing Typescript in tandem with an external library through es6 modules

Is there a recommended method for incorporating Typescript with non-module libraries like PixiJS and SortableJS without using webpacker? I'm looking to utilize es6 modules but want to avoid cumbersome solutions. What would be the best approach in this ...