Sending a generalized data type as a parameter for a type

In the process of developing a utility function to merge two TypeScript lists, I encountered the following challenge:

type TupleCombine<L extends unknown[], R extends unknown[]> =
    L extends [infer LH, ...infer LT]
    ? R extends [infer RH, ...infer RT]
    ? [LH & RH, ...TupleCombine<LT, RT>] // Merging tuples with 1+ elements
    : [LH & R[number], ...TupleCombine<LT, R>] // Merging tuple with list
    : R extends [infer RH, ...infer RT]
    ? [RH & L[number], ...TupleCombine<L, RT>] // Merging list with tuple
    : L extends []
    ? R extends []
    ? [] // Merging empty tuples
    : R[number] // Merging empty tuple with list 
    : R extends []
    ? L[number] // Merging empty list with tuple
    : (L[number] & R[number])[]; // Merging list types

I am now seeking a way to generalize this utility function so that I can avoid repeating similar cases for scenarios like "tuple unions" and other combinations involving tuple types.

My initial idea for this generalized version is as follows:

type TupleMerge<L extends unknown[], R extends unknown[], Combinator, Default> =
    L extends [infer LH, ...infer LT]
    ? R extends [infer RH, ...infer RT]
    ? [Combinator<LH, RH>, ...TupleMerge<LT, RT, Combinator, Default>]
    : [Combinator<LH, R[number]>, ...TupleMerge<LT, R, Combinator, Default>]
    : R extends [infer RH, ...infer RT]
    ? [Combinator<L[number], R>, ...TupleMerge<L, RT, Combinator, Default>]
    : L extends []
    ? R extends []
    ? []
    : [Combinator<Default, R[number]>]
    : R extends []
    ? [Combinator<L[number], Default>]
    : Combinator<L[number], R[number]>[];

Theoretically, I would specify a Combinator generic type in the following manner:

type Intersect<L, R> = L & R;
type TupleIntersect<L extends unknown[], R extends unknown[]> = TupleMerge<L, R, Intersect, unknown>;

type Union<L, R> = L | R;
type TupleIntersect<L extends unknown[], R extends unknown[]> = TupleMerge<L, R, Union, never>;

However, the TypeScript compiler does not permit this due to the restriction that "type parameters aren't generic types."

Is there a legitimate approach to achieving my objective using TypeScript? Or have I delved too deep into comparing TypeScript generic types with C++ template metaprogramming and template templates?

Answer №1

One way to achieve your goal is by utilizing free-types library

import { Lift, $Intersect, $Unionize } from 'free-types';

type Foo = { a: 1 };
type Bar = { b: 2 };
type Baz = { c: 3 };
type Qux = { d: 4 };

type I = Lift<$Intersect, [[Foo, Bar], [Baz, Qux]]>
// type I = [Foo & Baz, Bar & Qux]

type U = Lift<$Unionize, [[Foo, Bar], [Baz, Qux]]>
// type U = [Foo | Baz, Bar | Qux]

playground

An update on jcalz' answer: the declaration merging method is no longer in extensive use. A notable package still applying this pattern is fp-ts, with gradual changes in the new version fp-ts/core.

The prevalent approach involves intersecting an interface with an object so that the this keyword within the interface references the members of the intersected object. This diminishes the need for registering types in numerous interfaces for various type parameter variants or implementing convoluted methods like using strings as parameters, keyof operations, and more.

A straightforward implementation:

type Type = { [k: number]: unknown, type: unknown }
type apply<$T extends Type, Args> = ($T & Args)['type'];

interface $Union extends Type { type: this[0] | this[1] }
interface $Intersection extends Type { type: this[0] & this[1] }

type A = { a: 1 };
type B = { b: 2 };

type UnionAB = apply<$Union, [A, B]> // { a: 1 } | { b: 2 }
type IntersectionAB = apply<$Intersection, [A, B]> // { a: 1 } & { b: 2 }

With free-types, which provides support for type constraints to some extent, reimplementing TupleZip, $Unionize, and $Intersect can be done as follows:

...

Some benefits of this strategy over interface merging and module augmentation include:

  • Bypassing the use of magic strings which can confuse users. By directly passing a type constructor, it aligns more with higher-order functions, making it easier to understand and work with.

  • A contract can specify an explicit return type, unlike interfacing indexing which only considers the legality at the point of usage. The user has control over their free type's contract, allowing for inverted dependencies.

  • In cross-package scenarios, when utilizing interface merging, there may be issues with checking external packages loaded into the IDE workspace. Using a contract allows for comprehensive type-checking at the definition site of the free type, regardless of external factors.

Answer №2

One of the missing features in TypeScript is direct support for higher kinded types, where generic type parameters can themselves be generic. While there has been a long-standing request for this at microsoft/TypeScript#1213, it has not yet been implemented.

To work around this issue, one approach mentioned is to create a "registry" interface where higher kinded types can be placed and accessed using specific keys instead of names. Instead of defining types like:

type Intersect<L, R> = L & R;
type TupleIntersect<L extends unknown[], R extends unknown[]> =
  TupleZip<L, R, Intersect, unknown>;

type Union<L, R> = L | R;
type TupleIntersect<L extends unknown[], R extends unknown[]> =
  TupleZip<L, R, Union, never>;

You can use an interface and key referencing method like below:

interface Combinator<L, R> {
  Intersect: L & R;
}
type TupleIntersect<L extends unknown[], R extends unknown[]> =
  TupleZip<L, R, "Intersect", unknown>;
    
interface Combinator<L, R> {
  Union: L | R
}
type TupleUnion<L extends unknown[], R extends unknown[]> = 
  TupleZip<L, R, "Union", never>;

This requires updating your TupleZip definition to reflect these changes. You could also create utility types like ApplyCombinator to simplify things further:

type ApplyCombinator<K extends keyof Combinator<any, any>, L, R> =
  Combinator<L, R>[K];

type Demo = ApplyCombinator<"Intersect", Foo, Bar>
// type Demo = Foo & Bar

With that in place, you can test it out with examples like:

type I = TupleIntersect<[Foo, Bar], [Baz, Qux]>;
// type I = [Foo & Baz, Bar & Qux]
type U = TupleUnion<[Foo, Bar], [Baz, Qux]>;
// type U = [Foo | Baz, Bar | Qux]

All seems well!

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

Undefined 'require' error in Angular 2.0

I've been trying to get the Angular 2.0 Quickstart for Typescript running in Visual Studio 2015 by following the instructions on Angular 2.0 Quickstart, but I've run into some difficulties. After resolving the Typescript issues by transferring th ...

Leverage the app.component function within an Ionic 2 page

Currently facing an issue where I am attempting to utilize the app.component.ts function on a page, but encountering an error. EXCEPTION: Uncaught (in promise): TypeError: undefined is not an object (evaluating 'e.pages.updatePages') The functi ...

Using TypeScript: Applying constraints to generic types with primitive datatypes

My TypeScript code includes some generic classes: type UserId = number type Primitive = string | number | boolean class ColumnValue<T, S extends Primitive> { constructor(public columnName: String, public value: S) { } } abstract class Column<T ...

What is the best way to implement asynchronous guarding for users?

Seeking assistance with implementing async route guard. I have a service that handles user authentication: @Injectable() export class GlobalVarsService { private isAgreeOk = new BehaviorSubject(false); constructor() { }; getAgreeState(): Obser ...

Struggling with data interpolation issues in Angular2+ while using Angular material table

I am having trouble adjusting the sample material data table to accommodate my own data... This is how my data is structured: export const DATA: any = { 'products': [ { 'id': 1, 'name': 'SOLID BB T-SHI ...

Is there a way to set a default value for an Angular service provider?

Imagine an Angular Service that encapsulates the HTTP Client Module. export class HttpWrapperService { private apiKey: string; } Of course, it offers additional features that are not relevant here. Now I'm faced with the task of supplying HttpWr ...

A method to simultaneously retrieve all emitted values from multiple EventEmitters in Angular 7

I am facing a scenario where I have a parent component that consists of multiple child components. Each child component may differ from the others, creating a diverse structure within the parent component. Here's an example: ...

Create generic functions that prioritize overloading with the first generic type that is not included in the parameters

I encountered an issue while utilizing generic overload functions, as demonstrated below in the playground. The generic type T1 is solely used in the return type and not the parameters. Therefore, when attempting to use overload #2, I am required to speci ...

Upon completion of a promise in an express middleware and breaking out of a loop, a 404 error is returned

In my efforts to retrieve an array of object (car) from express using database functions in conjunction with the stolenCarDb object, everything seems to be working fine. However, when attempting the following code snippet, it results in a 404 error w ...

The type 'typeof globalThis' does not have an index signature, therefore the element is implicitly of type 'any'. Error code: ts(7017) in TypeScript

I'm encountering an issue with my input handleChange function. Specifically, I am receiving the following error message: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.ts(7017) when att ...

Enable Ace Editor's read-only mode

I'm having some difficulty getting my ace-editor to work in read-only mode. I'm working with Angular 9 and I've attempted the following approach: In my HTML file, I have the following code: <ace-editor mode="java" theme="m ...

Turn off the warning message that says 'Type of property circularly references itself in mapped type' or find a solution to bypass it

Is there a way to disable this specific error throughout my entire project, or is there a workaround available? The error message states: "Type of property 'UID' circularly references itself in mapped type 'Partial'.ts(2615)" https:/ ...

Issue encountered while setting up a socket server using socket.io in a TypeScript environment

As a beginner in the realm of socket.io, I embarked on a project where TypeScript is utilized. However, I encountered an error when starting the socket server with regards to importing. Despite attempting to rectify the issue by changing the import stateme ...

How can I enhance the mongoose Query class using Typescript?

I'm in the process of setting up caching using Mongoose, Redis, and Typescript. Here's a snippet from my cache.ts file : import mongoose, { model, Query } from "mongoose"; import redis from "redis"; //import { CacheOptions } f ...

I encountered numerous type errors while working on a React Native project that utilizes TypeScript

After following the instructions in the documentation to create a sample project, I used the command below: npx react-native init MyApp --template react-native-template-typescript Upon starting the project and running the command tsc I encountered 183 er ...

Guide on utilizing the main development dependencies in Vue 3 Mono Repo

After setting up a mono repo using Ionic and Vue 3, I encountered an issue where I had to manually add devDependencies into the package.json file of each child app. Is there a way to utilize the root devDependencies instead? "devDependencies": { ...

The express-validator library raises errors for fields that contain valid data

I've implemented the express-validator library for validating user input in a TypeScript API. Here's my validation chain: export const userValidator = [ body("email").isEmpty().withMessage("email is required"), body(&quo ...

Experiencing trouble accessing a property in TypeScript

While working on my Next.js project, I have encountered a specific issue related to selecting the Arabic language. The translation functions correctly and the text is successfully translated into Arabic. However, the layout does not switch from its default ...

Decorators: A Handy Tool for Renaming Instance Keys

I have a scenario where I have a class defined as follows: class A { description: string } My requirement is that when creating an instance of this class, I want to set the description attribute. However, when accessing the instance of the class, I woul ...

What's Going on with My Angular Dropdown Menu?

Snippet of HTML code: <li class="nav-item dropdown pe-3"> <a class="nav-link nav-profile d-flex align-items-center pe-0" (click)="toggleProfileMenu()"> <img src="assets/img/profile-img.jpg" alt=& ...