An array in Typescript containing elements of type MyType<T> with T being different for each element

TL;DR

Is there a way to create an array of tuples where the second element is automatically derived from the first?

Check out the example playground to see the code below in action.

My scenario

I'm using a tool called Puppeteer to navigate through various Sudoku websites, solve the puzzles, and move on to the next site.

Each website is defined by a type called Puppet<TDiff>, where TDiff represents a specific difficulty level for that particular site. I have a function named runPuppet that takes a Puppet as input along with some options, including the difficulty level, and then executes the algorithm mentioned above.

run-puppet.ts

interface Puppet<TDiff extends string | undefined = undefined> {
   // ...
}

interface RunOptions<TDiff extends string | undefined> {
   difficulty: TDiff;
   newGame: boolean;
   // ...
}

const default options = {
   newGame: false,
   // difficulty is not defined
   // ...
};

export default async function runPuppet<TDiff extends string | undefined = undefined>(
    puppet: Puppet<TDiff>,
    options: Partial<RunOptions<TDiff>>
) {
    options = Object.assign({}, defaultOptions, options);
    // ...
}

sudoku-com.ts (for sudoku.com)

type Difficulty = 'easy' | 'medium' | 'hard' | 'expert';

const SudokuDotComPuppet: Puppet<Difficulty> = {
    // ...
}

websudoku-com.ts (for websudoku.com)

type Difficulty = 'easy' | 'medium' | 'hard' | 'evil';

const WebSudokuDotComPuppet: Puppet<Difficulty> = {
    // ...
}

main.ts

(async () => {
    await runPuppet(WebSudokuDotComPuppet, { difficulty: 'evil' });
    await runPuppet(WebSudokuDotComPuppet, { difficulty: 'evil', newGame: true });
    await runPuppet(WebSudokuDotComPuppet, { difficulty: 'evil', newGame: true });
    await runPuppet(SudokuDotComPuppet, { difficulty: 'expert' });
    await runPuppet(SudokuDotComPuppet, { difficulty: 'expert', newGame: true });
    await runPuppet(SudokuDotComPuppet, { difficulty: 'expert', newGame: true });
})();

The current code successfully runs both puppets multiple times without any issues.

Now, my goal is to refactor the code in main.ts to utilize an array of tuples:

Array<[Puppet<TDiff>, TDiff]>
, where each tuple has its own unique difficulty value. This will allow me to achieve the following:

// needs refinement
type PuppetDifficulty<TDiff extends string | undefined = undefined> =
    [Puppet<TDiff>, TDiff];

(async () => {
    // throws compile-time errors
    const puppets: PuppetDifficulty[] = [
        [ WebSudokuDotComPuppet, 'evil' ],
        [ SudokuDotComPuppet, 'expert' ],
    ];

    for (const [puppet, difficulty] of puppets) {
        for (let i = 0; i < 3; i++) {
            await runPuppet(puppet, { difficulty, newGame: !!i });
        }
    }
})();

This snippet generates four errors indicating that 'expert' and 'evil' are not compatible with 'undefined'. This issue arises because when Puppet does not have a <TDiff> specified, it defaults to 'undefined' instead of inferring it based on the provided arguments.

I attempted to use the ElementType<T> pattern:

type DifficultyType<TPuppet extends Puppet> =
    TPuppet extends Puppet<infer T> ? T : undefined;

type PuppetDifficulty<TPuppet extends Puppet = Puppet> = [ TPuppet, DifficultyType<TPuppet> ];

(async () => {
    const puppets: PuppetDifficulty[] = [
        [ WebSudokuDotComPuppet, 'evil' ],
        [ SudokuDotComPuppet, 'expert' ],
    ];

    // ...
)();

However, this leads to the same set of errors encountered previously.

Answer №1

While I may not have the luxury of time to provide a thorough explanation, here is a brief overview. The main concept involves creating a general helper function with a generic type parameter that corresponds to a tuple representing the first element in each pair. This tuple type is then mapped to the actual pairs being passed in, leveraging TypeScript's ability to infer types from mappings or issue warnings if inference is ambiguous. Various nuances exist regarding guiding the compiler to interpret parameters like ["a", 1] as [string, number] rather than Array<string | number>, ensuring correct inference, among other considerations. Here is one approach:

const puppetDifficulties = <P extends Array<Puppet<any>>>(
  arr: [] | { [I in keyof P]: [P[I], P[I] extends Puppet<infer S> ? S : never] }
) => arr as Exclude<typeof arr, []>;

Here is how it can be used:

const puppets = puppetDifficulties([
    [Puppet1, "expert"],
    [Puppet2, "evil"],
    [Puppet1, "XXXX"] // error! not assignable to [Puppet<Puppet1Diff>, Puppet1Diff]
]);

Although puppets is strongly typed, iterating over it as a regular array compromises type safety (e.g., iterating over a tuple of type [string, number, boolean] will yield elements of type string | number | boolean). As a result, errors are avoided because items are interpreted as unions:

for (const [puppet, difficulty] of puppets) {
    for (let i = 0; i < 3; i++) {
        runPuppet(puppet, { difficulty, newGame: !!i });
    }
}

This information should provide some clarity and guidance. Best of luck!

Code Source

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

The Nest.js Inject decorator is not compatible with property-based injection

I am facing an issue with injecting a dependency into an exception filter. Here is the dependency in question: @Injectable() export class CustomService { constructor() {} async performAction() { console.log('Custom service action executed ...

An obstacle encountered when implementing feature module services in a controller for a Nest JS microservice

Recently, I developed a feature module named "user" which includes a controller, model, and services to interact with my postgres database. Despite setting up everything correctly, I encountered an error when trying to call userService from the feature mod ...

What is the method for generating an observable that includes a time delay?

Question In order to conduct testing, I am developing Observable objects that simulate the observable typically returned by an actual http call using Http. This is how my observable is set up: dummyObservable = Observable.create(obs => { obs.next([ ...

Is there a more efficient method to specify to typescript the type of "data"?

Recently, I've adopted the action/reducer pattern for React based on Kent Dodds' approach and now I'm exploring ways to enhance type safety within it. export type Action = { type: "DO_SOMETHING", data: { num: Number } } | ...

Issues arise in Ionic 3 when attempting to use scripts or external custom jQuery plugins within inner pages

When I utilize a script tag in my index.HTML file, it functions properly on the initial or root pages of Ionic 3. However, upon navigating to other pages using NavController, the script ceases to work on these inner pages. How can I implement a custom jQ ...

Troubleshooting data binding problems when using an Array of Objects in MatTableDataSource within Angular

I am encountering an issue when trying to bind an array of objects data to a MatTableDataSource; the table displays empty results. I suspect there is a minor problem with data binding in my code snippet below. endPointsDataSource; endPointsLength; endP ...

Ensuring TypeScript's strict null check on a field within an object that is part of an

When using TypeScript and checking for null on a nullable field inside an object array (where strictNullCheck is set to true), the compiler may still raise an error saying that 'Object is possibly undefined'. Here's an example: interface IA ...

Trouble navigating cursor position on ngModelChange in Angular/Typescript

I'm currently facing an issue with my HTML input field combined with a typescript component utilizing ngModelChange. I am aiming to have the flexibility to edit the input value wherever necessary. Let's consider this scenario: The original inpu ...

Creating multiple copies of a form div in Angular using Typescript

I'm attempting to replicate a form using Angular, but I keep getting the error message "Object is possibly 'null'". HTML: <div class="form-container"> <form class="example"> <mat-form-field> ...

Differences in the treatment of Map objects by Angular TypeScript and JavaScript

Here is an example of an interface I have defined: export interface Parameter { access: string; value: string; } export interface Parameters { parameter: Map<string, Parameter>; } In my code, I am trying to use the above interface like thi ...

Creating HTML Elements using Typescript's Syntax

Looking for the most effective method to define HTML elements in typescript is a challenge I am facing. One particular issue that keeps arising is when dealing with arrays of DOM nodes retrieved using document.querySelectorAll. The type assigned to these e ...

Tips for patiently waiting for a method to be executed

I have encountered a situation where I need to ensure that the result of two methods is awaited before proceeding with the rest of the code execution. I attempted to use the async keyword before the function name and await before the GetNavigationData() me ...

Locate the minimum and maximum values between two inputted dates

I'm looking for a solution that provides strongly typed code. The problem arises when trying to implement solutions from a related question - Min/Max of dates in an array? - as it results in an error. TS2345: Argument of type 'Date' is not ...

Establish a default value for ng2-datepicker

Has anyone figured out how to set an initial value for the ng2-datepicker when using it for available date and date expires? I want the initial value of dateAvailable to be today's date and the initial value of dateExpires to be 2099-12-31. <label ...

React/Typescript/VScode - a '.tsx' extension cannot be used at the end of an import path

I have successfully converted a series of React projects to TypeScript, but I am encountering a specific issue with one non-webpack project. The error I am facing is 'an import path cannot end with a .tsx extension'. For example, this error occur ...

Is there a way to divide the array based on a specific letter in the alphabet using Angular?

I am trying to create something similar to this: "ABCDEF", "GHIJK", "LMNO", "PRSTU", "VYZXWQ", "0123456789" I have a list in alphabetical order; I want names starting with "ABCDEF" to be in one a ...

Is it possible to incorporate regular React JSX with Material UI, or is it necessary to utilize TypeScript in this scenario?

I'm curious, does Material UI specifically require TypeScript or can we use React JSX code instead? I've been searching for an answer to this question without any luck, so I figured I'd ask here. ...

Transferring data to a different module

I'm currently working on an Angular program where I am taking a user's input of a zip code and sending it to a function that then calls an API to convert it into latitude and longitude coordinates. Here is a snippet of the code: home.component.h ...

Tips for enhancing Mui 5 Typography using personalized properties (TypeScript)

I am attempting to include a custom property in Mui's Typography component: using module augmentation: // mui.d.ts declare module "@mui/material/Typography" { interface TypographyProps { opacity: string | number; } } with theme co ...

Mapping with Angular involves iterating over each element in an array and applying a function to each one

I am working with an API that provides data in a specific format: {"data": [ { path: "some path", site_link: "link", features: ['feature1', 'feature2'] } ... ]} Now, I have a service called getSites() ge ...