Derive a generic intersection type by extracting various types from an array within a function argument

I am in the process of extracting a common type that combines the types of objects found within an array passed as an argument to a function. To better explain this concept, consider the following code:

type Extension = {
  name: string,
  fields: {
    [key: string]: string
  }
}

type Config = {
  name: string,
  extensions: Extension[]
}

const ext1: Extension = {
  name: 'ext1',
  fields: {
    field1: 'test',
    field2: 'test'
  }
}

const ext2: Extension = {
  name: 'ext1',
  fields: {
    field3: 'test',
    field4: 'test'
  }
}

const config: Config = {
  name: 'Test',
  extensions: [ext1, ext2]
}

// Disregarding the obvious incorrect typings here
function create(config: Config): T { ... }

Is there a way to ensure that T has the specified type below?

type CombinedFields = {
  field1: string,
  field2: string,
} & {
  field3: string,
  field4: string
}

Answer №1

For this scenario to work, the compiler must keep track of specific keys and values for ext1, ext2, and config. However, if you use broad types like Extension and Config as annotations on those variables, you will lose the necessary information for tracking them. Therefore, avoiding annotations is crucial. Instead, utilize the satisfies operator to validate that the variables are assignable to those types without widening them:

const ext1 = {
  name: 'ext1',
  fields: {
    field1: 'test',
    field2: 'test'
  }
} satisfies Extension;

/* const ext1: {
    name: string;
    fields: {
        field1: string;
        field2: string;
    };
} */

const ext2 = {
  name: 'ext1',
  fields: {
    field3: 'test',
    field4: 'test'
  }
} satisfies Extension;

/* const ext2: {
    name: string;
    fields: {
        field3: string;
        field4: string;
    };
} */

These types now have knowledge about the field names. When it comes to config, we need to take additional steps; it's essential for extensions to maintain the length and order of its elements, distinguishing between [ext1, ext2] and

[Math.random()<0.5 ? ext1 : ext2]
(or at least I assume this distinction is required). This means applying a const assertion on config and allowing Config's extensions property to be a readonly array type due to the benefits of const assertions:

type Config = {
  name: string,
  extensions: readonly Extension[]
}
const config = {
  name: 'Test',
  extensions: [ext1, ext2]
} as const satisfies Config;

/* const config: {
    readonly name: "Test";
    readonly extensions: readonly [{
        name: string;
        fields: {
            field1: string;
            field2: string;
        };
    }, {
        name: string;
        fields: {
            field3: string;
            field4: string;
        };
    }];
} */

With this information in place, we can proceed with confidence.


One possible solution is outlined below:

type ExtensionsToIntersection<T extends readonly Extension[]> =
  { [I in keyof T]: (x: T[I]["fields"]) => void }[number] extends
  (x: infer I) => void ? I : never;

declare function create<T extends Config>(config: T):
  ExtensionsToIntersection<T["extensions"]>;

The

ExtensionsToIntersection<T>
type transforms a tuple containing elements assignable to Extension into an intersection of the fields properties of those elements. The concept aligns closely with the approach detailed in response to TypeScript merge generic array. Essentially, by mapping the elements to intersect within a contravariant type position and inferring a single type from the union of those mappings, we achieve the desired intersection of parameter types.

Let's put this solution to the test:

const ret = create(config);
/* const ret: {
    readonly field1: "test";
    readonly field2: "test";
} & {
    readonly field3: "test";
    readonly field4: "test";
} */

Everything appears to be functioning correctly!

Playground link to code

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 could be causing the headings and lists to not function properly in tiptap?

I'm currently working on developing a custom text editor using tiptap, but I've encountered an issue with the headings and lists functionalities not working as expected. View the output here 'use client'; import Heading from '@tip ...

Can Angular components be used to replace a segment of a string?

Looking to integrate a tag system in Angular similar to Instagram or Twitter. Unsure of the correct approach for this task. Consider a string like: Hello #xyz how are you doing?. I aim to replace #xyz with <tag-component [input]="xyz">&l ...

Angular 4's unique feature is the ability to incorporate multiple date pickers without the

Is there a way to implement a multiple date picker using Angular 4 and TypeScript only? I came across one solution but it seems to only support Angular 1. Thank you in advance! ...

No matter which port I try to use, I always receive the error message "listen EADDRINUSE: address already in use :::1000"

Encountered an error: EADDRINUSE - address already in use at port 1000. The issue is within the Server setupListenHandle and listenInCluster functions in the node.js file. I am currently running this on a Mac operating system, specifically Sonoma. Despit ...

React's componentDidUpdate being triggered before prop change occurs

I am working with the CryptoHistoricGraph component in my app.js file. I have passed this.state.coinPrices as a prop for this element. import React from 'react'; import axios from 'axios'; import CryptoSelect from './components/cry ...

Tips on narrowing down the type of callback event depending on the specific event name

I've been working on implementing an event emitter, and the code is pretty straightforward. Currently, tsc is flagging the event type in eventHandler as 'ErrorEvent' | 'MessageEvent'. This seems to be causing some confusion, and I ...

What is the correct way to interpret a JSON file using TypeScript?

Encountering Error Error TS2732: Cannot locate module '../service-account.json'. It is suggested to use the '--resolveJsonModule' flag when importing a module with a '.json' extension. import serviceAccountPlay from '../ ...

Retrieve both the name and id as values in an angular select dropdown

<select (change)="select($event.target.value)" [ngModel]="gen" class="border border-gray-200 bg-white h-10 pl-6 pr-40 rounded-lg text-sm focus:outline-none appearance-none block cursor-pointer" id="gend ...

When using MERN Stack (with Typescript) on DigitalOcean, encountering an issue where HTML files are displayed instead of JS and

Upon checking the console, I encountered this https://i.sstatic.net/PWoT5.jpg The app has been developed using Ubuntu and Nginx so far with no firewall configuration yet in place. This is my first time deploying a MERN stack and utilizing DigitalOcean. ...

In the TypeScript handbook, when it mentions "can be considered as the interface type," what exactly is meant by that?

The manual emphasizes that: It’s crucial to understand that an implements clause is merely a confirmation that the class can be used as if it were of the interface type. It does not alter the class's type or methods in any way. One common mistake ...

Checkbox selections persist when navigating between pages

I am currently working with Angular 9 and I have a list of checkboxes that need to default to true when displaying certain data. If one of these checkboxes is unchecked, it should trigger the display of specific information. The issue I am facing is that o ...

React Native - The size of the placeholder dictates the height of a multiline input box

Issue: I am facing a problem with my text input. The placeholder can hold a maximum of 2000 characters, but when the user starts typing, the height of the text input does not automatically shrink back down. It seems like the height of the multiline text ...

What is the best way to enforce input requirements in Typescript?

I am currently facing an issue with two required inputs that need to be filled in order to enable the "Add" button functionality. I have considered using *ngIf to control the visibility of the button based on input values, but it seems to not be working. ...

Every time an action is carried out in the app, React generates countless TypeError messages

Whenever I'm using the application (particularly when started with npm start), my console gets flooded with thousands of TypeError messages like this: https://i.sstatic.net/3YZpV.png This issue doesn't occur when I build the app... It's fr ...

What is the correct way to initialize and assign an observable in Angular using AngularFire2?

Currently utilizing Angular 6 along with Rxjs 6. A certain piece of code continuously throws undefined at the ListFormsComponent, until it finally displays the data once the Observable is assigned by calling the getForms() method. The execution of getForm ...

Executing a function defined within an iframe from the parent component by utilizing React Ref

Within my React code, I have an iframe that I'm attempting to access from the parent component. I've set up a React Ref to connect to the iframe, but I'm unsure how to interact with the functions inside the iframe from the React component. H ...

Is there a way for me to retrieve the aria-expanded attribute value from a button element

Is there a way to access button properties from a click listener in order to use the aria-expanded attribute to set certain values? Here is my current code. x.html <button class="btn btn-secondary" (click)="customSearch($event.target)" type="button" d ...

Using React with TypeScript to iterate over an array and add corresponding values from an object based on a common key

When working with regular JavaScript, I can achieve the following... var array1 = ['one', 'two', 'three', 'four']; var object = { one: 'apple', two: 'orange', three: 'peach', fo ...

The custom native date adapter is facing compatibility issues following the upgrade of Angular/Material from version 5 to 6

In my Angular 5 application, I had implemented a custom date adapter as follows: import {NativeDateAdapter} from "@angular/material"; import {Injectable} from "@angular/core"; @Injectable() export class CustomDateAdapter extends NativeDateAdapter { ...

Angular component unable to access Service provided via Dependency Injection

After diving into an Angular tutorial yesterday (), I reached a point where services came into play. The Service class, named MediaServiceService and housed in the media-service.service.ts file, had the following structure: import { Injectable } from &ap ...