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),
  };
}

return makeNexter({}, async (data) => {

  return makeNexter({ ...data, a: 3 }, async (data) => {

    return makeNexter({ ...data, b: 'hi' }, async (data) => {

    });

  });

});

Is there a method to create a function that can handle an indefinite number of these functions in an array and execute each result in sequence while maintaining type information for each data parameter within the inner function?

The goal is to transform this pyramid code into a flat structure without losing any type information.

One way to think about it is building a generator function where the output types are known without needing extra assertions or filtering at each step but pass linearly through each stage with certainty.

Can such a function be implemented in TypeScript with correct type inference?


/** @type {???} */
return makeNexters({}, [
  async (data) => {
    return { ...data, a: 3 };
  },
  async (data) => {
    return { ...data, b: 'hi' };
  },
  async (data) => {
    // The data here should have type:
    //   {
    //     b: string;
    //     a: number;
    //   }
  },
]);

Check out my feature request on TypeScript issues: https://github.com/microsoft/TypeScript/issues/43150

Answer №1

Creating a solution where the compiler automatically infers types as desired without additional effort when calling the function seems difficult.

There is an identified design limitation in TypeScript, discussed in microsoft/TypeScript#38872, where the compiler struggles to simultaneously infer generic type parameters and contextual types for callback parameters that depend on each other. When making a call like:

return makeNexters({}, [
  async (data) => { return { ...data, a: 3 }; },
  async (data) => { return { ...data, b: 'hi' }; },
  async (data) => {},
])

The request to the compiler involves using {} for inferring some generic type parameter, which then influences the type of the first data callback parameter, subsequently affecting the inference of another generic type parameter linked to the next data callback parameter, and so forth. The compiler can only handle a limited number of type inference phases before giving up after potentially the initial one or two inferences.


Ideally, I propose expressing the type of makeNexters() somewhat like this:

// Type definition for makeNexters
type Idx<T, K> = K extends keyof T ? T[K] : never

declare function makeNexters<T, R extends readonly any[]>(
  init: T, next: readonly [...{ [K in keyof R]: (data: Idx<[T, ...R], K>) => Promise<R[K]> }]
): void;

This type specification indicates that the init parameter has a generic type T, while the next parameter functions with tuple type mapping R. Each element within next should be a function accepting a data parameter from the "previous" element in R (except for the first one accepting it from

T</code), and returning a <code>Promise
for the "current" element in R.

(The void return value concern isn't pivotal here at present due to focusing on type inference)

This method does function effectively but not exactly in the preferred way of type inference. It's possible to prioritize contextual type inference for callback parameters over generic type inference:

// Prioritizing contextual type inference
makeNexters<{}, [{ a: number; }, { b: string; a: number; }, void]>() 

Or vice versa, emphasizing generic type inference at the expense of contextual type inference:

// Emphasizing generic type inference
makeNexters<{}, [{ a: number }, { b: string, a: number }, void]>({})

An attempt to achieve both types of inference concurrently results in ineffective any inferences throughout:

// Ineffective mixed inference
makeNexters({}, [
  async (data) => { return { ...data, a: 3 }; },
  async (data) => { return { ...data, b: 'hi' }; },
  async (data) => { }
]);
/* function makeNexters<{}, [any, any, void]>*/

Situations requiring manual input for typing like {b: string, a: number} diminish the purpose behind this chained function concept.


Prior to conceding completely, consider altering the strategy towards a nested architecture where each link in the chain originates from a separate function call. This aligns with the builder pattern, constructing the "nexter" incrementally instead of all at once via a single array representation:

// Alternative approach using builder pattern
const p = makeNexterChain({})
  .and(async data => ({ ...data, a: 3 }))
  .and(async data => ({ ...data, b: "hi" }))
  .done

The resulting p object type would match the original nested version:

// Output type matching nested version
/* const p: {
    data: {};
    next: () => Promise<{
        data: {
            a: number;
        };
        next: () => Promise<{
            data: {
                b: string;
                a: number;
            };
            next: () => Promise<void>;
        }>;
    }>;
} */

The implementation details of makeNexterChain() fall beyond the scope here, focused more on typings rather than runtime behavior. Construct makeNexterChain() considering appropriate utilization of promise's then() method.

In essence, makeNexterChain() starts with an initial element of type T, producing a NexterChain<[T]>. Each NexterChain<R> (with R being a tuple type) offers an and() method to append a new type to the end of R, alongside a done() method returning a Nexter<R>. A Nexter<R> includes a data property reflecting the first element of R, along with a next() method sans arguments, generating a Promise for a fresh Nexter<T> identical to

R</code minus the initial element. Eventually leading to <code>void
at termination.

This demonstrated method exhibits exceptional type inference clarity at each step, alleviating the need for explicit generic parameter definition or callback parameter annotation. Explore such solutions utilizing TypeScript's robust type inference functionality seamlessly.

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 is the best way to repurpose a variable in Angular's TypeScript?

I'm currently working on an application that utilizes the following technologies. In my Typescript file named "test.page.ts", there is a variable called "response: any" that I need to reuse in another Typescript file named "test2.page.html" by calling ...

I'm encountering a 502 error while trying to use Supabase's signInWIthPassword feature

Despite all authentication functions working smoothly in my React, TypeScript, and Supabase setup, I'm facing an issue with signInWithPassword. In my context: I can successfully signIn, create a profile, and perform other operations like getUser() an ...

When selecting the "Open Link in New Tab" option in Chrome, the Angular app's routing will automatically redirect to the login page

I am facing a peculiar issue in my Angular 2 application that I need help troubleshooting. Currently, the routing within my app functions as intended when I click on links to navigate between different components. Here is an example of how the routing path ...

The best way to access the value of a fulfilled Promise while debugging

I've been using this loopback application in my IntelliJ IDE. I set a breakpoint at the promise in calculator.controller.ts, where the code looks like this: @get('/multiply/{intA}/{intB}') async multiply( @param.path.integer('in ...

Tips for sending icons as properties in React using TypeScript

Recently diving into typescript, I embarked on a straightforward project. Within this project lies a sidebar component that comprises multiple sidebarNavigationItem components. Each of these Sidebar items consists of an Icon and Title, showcased below. Si ...

Tips for verifying the presence of a value within an array using checkboxes

My firestore database contains a collection named world with a sub-collection called languages I have developed two functions: one to retrieve all documents from the sub-collection languages, and another function to fetch every language if the userUid val ...

Creating a function that utilizes a default argument derived from a separate argument

Consider this JavaScript function: function foo({ a, b, c = a + b }) { return c * 2; } When attempting to add type annotations in TypeScript like so: function foo({ a, b, c = a + b }: { a?: number, b?: number, c: number }): number { return c * 2; } ...

Adding additional properties to Material UI shadows in Typescript is a simple process that can enhance the visual

https://i.stack.imgur.com/9aI0F.pngI'm currently attempting to modify the Material UI types for shadows, but encountering the following error when implementing it in my code. There is no element at index 25 in the tuple type Shadows of length 25. I&a ...

Incorporating a JavaScript file into Angular

I'm looking to incorporate a new feature from this library on GitHub into my Angular project, which will enhance my ChartJS graph. @ViewChild('myChart') myChart: ElementRef; myChartBis: Chart; .... .... const ctx = this.myChart.nativeEleme ...

Avoid saying the same thing more than once

Within my Typescript class, I have the following structure: class C { #fsm (...) startFoo(name: string) { this.#fsm.send('FOO', name) return this } startBar(name: string) { this.#fsm.send('BAR', name) return th ...

Utilizing Angular2 to scan barcodes

Im looking to create an application in asp.net 5 / Angular2 and I am encountering an issue with scanning barcodes. This is the TypeScript code for my component: @Component({ selector: 'barcode-scanner', templateUrl: 'app/scan.html& ...

Tailoring Aurelia for .cshtml integration

I stumbled upon an informative article detailing the integration of Razor partials (cshtml) with aurelia. Despite my efforts, I encountered difficulty in getting the code to execute properly and was informed by Rob Eisenberg's comment that Convention ...

npm unable to locate a JavaScript file

Currently, I am developing an Angular 2 application that utilizes the ng2-slugify package. However, I have encountered an issue where it cannot locate one of the required slugify files, specifically "charmaps.js", even though it is stored in the same direc ...

Interference of NestJS provider classes in separate event loops causing conflicts

I'm currently facing an issue where my shared library injectables are conflicting with each other. The bootstrap file initiates this file alongside a proxy server to start local microservices import { serviceA } from '@company/serviceA' imp ...

Is there a counterpart to ES6 "Sets" in TypeScript?

I am looking to extract all the distinct properties from an array of objects. This can be done efficiently in ES6 using the spread operator along with the Set object, as shown below: var arr = [ {foo:1, bar:2}, {foo:2, bar:3}, {foo:3, bar:3} ] const un ...

Having completed "npm link" and "npm i <repo>", the module cannot be resolved despite the presence of "main" and "types" in the package.json file

Here is the contents of my package.json file: { "name": "ts-logger", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "install": "tsc" ...

Cannot access Injectable service in Angular2

In the angular2 application, there is a service named HttpClient. The purpose of this service is to include an authorization header in every request sent by the application to endpoints. import { Injectable } from '@angular/core'; import { He ...

TS2365: The '!== 'operator is not compatible with the types ""("" and "")""

function myFunction(identifier: string) { identifier = "("; }; let identifier: string = ")"; if (identifier !== '(') throw "Expected '(' in function"; myFunction(identifier); if (identifier !== ')') throw "Expected &a ...

Can we establish communication between the backend and frontend in React JS by utilizing localstorage?

Trying to implement affiliate functionality on my eCommerce platform. The idea is that users who generate links will receive a commission if someone makes a purchase through those links. However, the challenge I'm facing is that I can't store the ...

Using Visual Studio Code Build Tasks in Harmony

The documentation for Visual Studio Code includes examples of tasks.json configurations that allow for either typescript compilation or markdown compilation, but does not provide clear instructions on how to achieve both simultaneously. Is there a way to ...