What is the process for converting variadic parameters into a different format for the return value?

I am currently developing a combinations function that generates a cartesian product of input lists. A unique feature I want to include is the ability to support enums as potential lists, with the possibility of expanding to support actual Sets in the future.

Here is the code snippet that has been proven to work:

type EnumOrArray = { [key: string]: any; } | any[];

export function cartesian(...inputs: EnumOrArray[]): any[][] {
  const itemGroups: any[][] = inputs.map(input => {
    if (Array.isArray(input)) {
      return input; // Return directly if input is an array
    } else {
      // Extract enum keys and convert them to numeric values
      const originalKeys = Object.keys(input).filter(key => isNaN(Number(key)));
      return originalKeys.map(key => input[key]);
    }
  });

  // Recursive function for generating combinations (remains unchanged)
  function generateCartesianProduct(groups: any[][], prefix: any[] = []): any[][] {
    if (!groups.length) return [prefix];
    const firstGroup = groups[0];
    const restGroups = groups.slice(1);

    let result: any[][] = [];

    firstGroup.forEach(item => {
      result = result.concat(generateCartesianProduct(restGroups, [...prefix, item]));
    });

    return result;
  }

  return generateCartesianProduct(itemGroups);
}

However, the issue lies in the fact that this setup acts like a type firewall and only produces output of type any[][], which is not ideal.

For instance, when using the above code with enums like this:


  enum Color {
    Red = 'red',
    Green = 'green',
    Blue = 'blue'
  }
  enum Size {
    Small = 1,
    Medium = 10,
    Large = 100
  }
  const numbers = [1, 2];
  const combos = cartesian(Color, numbers, Size);

The output generated is correct:

[
  [ 'red', 1, 1 ],     [ 'red', 1, 10 ],
  [ 'red', 1, 100 ],   [ 'red', 2, 1 ],
  [ 'red', 2, 10 ],    [ 'red', 2, 100 ],
  [ 'green', 1, 1 ],   [ 'green', 1, 10 ],
  [ 'green', 1, 100 ], [ 'green', 2, 1 ],
  [ 'green', 2, 10 ],  [ 'green', 2, 100 ],
  [ 'blue', 1, 1 ],    [ 'blue', 1, 10 ],
  [ 'blue', 1, 100 ],  [ 'blue', 2, 1 ],
  [ 'blue', 2, 10 ],   [ 'blue', 2, 100 ]
]

However, my goal is to have the output type as [Color, number, Size][], rather than any[][].

I believe the solution lies in utilizing a variadic generic type effectively, but I am unable to figure it out at the moment.

Answer №1

For the following discussion, I will assume that the function cartesian has already been implemented and you are only interested in defining the call signature types. The compiler will not be able to verify if the implementation matches the call signature, so within the function, you may need to use type assertions, the any type, or similar methods to avoid compiler errors.


To approach this problem, I suggest making the function generic based on the tuple type T of the inputs array. Then, have the function return an array of a mapped array type that converts each element of T into the corresponding output type:

declare function cartesian<T extends EnumOrArray[]>(...inputs: T):
  { [I in keyof T]: ConvertEnumOrArrayToElement<T[I]> }[];

Therefore, if inputs is of type [A, B, C], the return type will be

[ConvertEnumOrArrayToElement<A>, ConvertEnumOrArrayToElement<B>, ConvertEnumOrArrayToElement<C>][]
. Now, let's define the ConvertEnumOrArrayToElement function.


The objective of

ConvertEnumOrArrayToElement<T>
is for T to represent a subtype of EnumOrArray. If T is an array of type U[], then we want U. If not, assuming T to be of type {[k: string]: any}, our aim is to obtain T[keyof T], which is essentially a union of property value types in
T</code while handling numeric keys appropriately to prevent reverse mappings for numeric enums.</p>
<pre><code>type ConvertEnumOrArrayToElement<T extends EnumOrArray> =
  T extends (infer U)[] ? U :
  { [K in keyof T]: K extends number ? never : T[K] }[keyof T]

In this scenario, conditional types are used to differentiate between whether T is an array or another object. Conditional type inference with infer helps extract the array element type

U</code. For non-array cases, a mapped type is applied to suppress properties associated with numeric keys, thus fulfilling the intended purpose.</p>
<hr />
<p>Testing time:</p>
<pre><code>enum Color {
  Red = 'red',
  Green = 'green',
  Blue = 'blue'
}

enum Size {
  Small = 1,
  Medium = 10,
  Large = 100
}
const numbers = [1, 2];
const combos = cartesian(Color, numbers, Size);
//    ^? const combos: [Color, number, Size][]

Everything seems in order with the expected output type obtained. To clarify further, here are the results of applying ConvertEnumOrArrayToElement to each element of inputs:

type ColorOutput = ConvertEnumOrArrayToElement<typeof Color>
// type ColorOutput = Color
type NumbersOutput = ConvertEnumOrArrayToElement<typeof numbers>
// type NumbersOutput = number
type SizeOutput = ConvertEnumOrArrayToElement<typeof Size>
// type SizeOutput = Size

Hence, arrays are converted to their respective element types, and enums maintain their enum type as needed.

Link to code in TypeScript Playground

Answer №2

This solution exceeded my expectations by fulfilling all the required criteria and even handling tuple types like ['a', 'b'] as const.

type EnumOrArray = { [key: string]: any; } | any[];

type ConvertEnumOrArrayToElement<T extends EnumOrArray> = T extends readonly (infer U)[] ? U :
{ [K in keyof T]: K extends number ? never : T[K] }[keyof T]

type ConvertArrayToElementAndEnumToKey<T extends EnumOrArray> = T extends readonly (infer U)[] ? U :
// in this section, we can't simply use keyof T to eliminate the number keys.
{ [K in keyof T]: K extends number ? never : K }[keyof T]

function enum_to_values<T extends { [key: string]: any }>(e: T): ConvertEnumOrArrayToElement<T>[] {
  return Object.keys(e).filter(k => isNaN(Number(k))).map(k => e[k]) as any;
}
function enum_to_keys<T extends { [key: string]: any }>(e: T): ConvertArrayToElementAndEnumToKey<T>[] {
  return Object.keys(e).filter(k => isNaN(Number(k))) as any;
}

// The recursive function for generating combinations remains unchanged
function generateCartesianProduct(groups: any[][], prefix: any[] = []): any[][] {
  if (!groups.length) return [prefix];
  const firstGroup = groups[0];
  const restGroups = groups.slice(1);
  return firstGroup.flatMap(item => generateCartesianProduct(restGroups, [...prefix, item]));
}

export const cartesian = <T extends EnumOrArray[]>(...inputs: T): { [I in keyof T]: ConvertArrayToElementAndEnumToKey<T[I]> }[] => generateCartesianProduct(inputs.map(inp => Array.isArray(inp) ? inp : enum_to_keys(inp))) as any;

export const cartesian_enum_vals = <T extends EnumOrArray[]>(...inputs: T): { [I in keyof T]: ConvertEnumOrArrayToElement<T[I]> }[] => generateCartesianProduct(inputs.map(inp => Array.isArray(inp) ? inp : enum_to_values(inp))) as any;

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

Encapsulate the module function and modify its output

I am currently utilizing the node-i18n-iso-countries package and I need to customize the getNames function in order to accommodate a new country name that I wish to include. At the moment, I am achieving this by using an if-else statement like so: let cou ...

Executing a function to erase the stored value in local storage during an Angular unit test

Looking to verify whether the localStorage gets cleared when I execute my function. Component ngOnInit() { // Logging out when reaching login screen for login purposes this.authService.logout(); } authService logout() { // Removing logged i ...

When converting an object into a specific type, the setter of the target type is not invoked

Imagine a scenario with a class structured like this: class Individual { private _name: string; get Name(): string { return this._name; } set Name(name: string) { this._name = name; } } Upon invoking HttpClient.get<Individual>(), it retrieve ...

Where specifically in the code should I be looking for instances of undefined values?

One method in the codebase product$!: Observable<Product>; getProduct(): void { this.product$ = this.route.params .pipe( switchMap( params => { return this.productServ.getById(params['id']) })) } returns an ...

Angular 4 Filtering Pipe: Simplify Data Filtering in Angular

Attempting to replicate AngularJS's OrderBy feature. Working with an array like this, I am aiming to filter the cars by their car category. [ { "car_category": 3, "name": "Fusion", "year": "2010" }, { "car_category": 2, "na ...

The IntrinsicAttributes type does not include the property 'path' in the Preact router

I am facing a challenge while developing my website using preact router. Every time I try to add something to the router, I encounter an error stating "Property 'path' does not exist on type 'IntrinsicAttributes'." Despite this error, t ...

Decorator used in identifying the superclass in Typescript

I am working with an abstract class that looks like this export abstract class Foo { public f1() { } } and I have two classes that extend the base class export class Boo extends Foo { } export class Moo extends Foo { } Recently, I created a custom ...

Error in GoogleMapReact with Next.js: TypeError occurs when trying to read properties of undefined, specifically 'getChildren'

Currently, I am working on a basic nextjs application using the google-map-react component and nextjs. However, I keep encountering an error whenever I try to utilize the component. The error message reads as follows: "TypeError: can't access propert ...

Is it possible to maintain the input and output types while creating a function chain factory in

Take a look at the following code snippet involving pyramids: /** * @template T, U * @param {T} data * @param {(data: T) => Promise<U>} fn */ function makeNexter(data, fn) { return { data, next: async () => fn(data), }; } retu ...

I am looking to display data in Angular based on their corresponding ids

I am facing a scenario where I have two APIs with data containing similar ids but different values. The structure of the data is as follows: nights = { yearNo: 2014, monthNo: 7, countryId: 6162, countryNameGe: "რუსეთის ...

Error in Typescript occurrence when combining multiple optional types

This code snippet illustrates a common error: interface Block { id: string; } interface TitleBlock extends Block { data: { text: "hi", icon: "hi-icon" } } interface SubtitleBlock extends Block { data: { text: &qu ...

Error during Webpack Compilation: Module 'jQuery' not found in Travis CI with Mocha Test

I've been struggling to automate tests for my repository using mocha-webpack and Travis CI. The local machine runs the tests smoothly, but Travis CI hasn't been able to complete them yet due to an unresolved error: WEBPACK Failed to compile wit ...

The function within filereader.onload is not running properly in JavaScript

When working with FileReader to read a file and convert it to base64 for further actions, I encountered an issue. Although I was able to successfully read the file and obtain its base64 representation, I faced difficulties in utilizing this data to trigger ...

What is the process for sending an email using Angular 6 and ASP.NET CORE web api?

When a user inputs their email and password in the designated boxes, they also include a recipient email address with a CC. This information is then sent via a web api. The email will contain text like "Total Sales Today" along with an attached PDF file. ...

The directive [ngTemplateOutet] is functioning properly, however, the directive *ngTemplateOutlet is not functioning as expected

I have been struggling to create a component using ngTemplateOutlet to select an ng-template, but for some reason *ngTemplateOutlet is not working in this case (although [ngTemplateOutlet] is functioning correctly). Below is the code I am currently using: ...

Explain the form of an object using typescript without considering the categories

I'm looking to define the shape of an object in TypeScript, but I want to disregard the specific types of its fields. interface TestInterface { TestOne?: string; TestTwo?: number; TestThree?: boolean; } My approach was to define it like this: ...

Issues with React Material UI Select functionality not performing as expected

I've been working on populating a Select Component from the Material UI library in React, but I'm facing an issue where I can't choose any of the options once they are populated. Below is my code snippet: import React, { useState, useEffect ...

Using Typescript to import an npm package that lacks a definition file

I am facing an issue with an npm package (@salesforce/canvas-js-sdk) as it doesn't come with a Typescript definition file. Since I am using React, I have been using the "import from" syntax to bring in dependencies. Visual Studio is not happy about th ...

What is a simple way to exclude a prop from a declaration in a React/TypeScript project in order to pass it as undefined

I'm trying to accomplish this task but encountering a typescript error indicating that the label is missing. Interestingly, when I explicitly set it as undefined, the error disappears, as shown in the image. Here's how my interface is structured ...

A guide on selecting the best UI container based on API data in React using TypeScript

I have been developing a control panel that showcases various videos and posts sourced from an API. One div displays video posts with thumbnails and text, while another shows text-based posts. Below is the code for both: <div className=""> &l ...