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

registering a back button action in Ionic2 for multiple pages

Currently, I am in the process of developing my Ionic2 app and have encountered a dilemma regarding the functionality of registerBackButtonAction. On one page, let's call it pageA, I have implemented this function and everything is functioning as exp ...

Fire the BehaviorSubject with the identical value following a mutation

I am working with a BehaviorSubject where I have to make changes through mutation (for reasons beyond my control). I need to trigger the BehaviorSubject for subscriptions whenever there are changes. Is there another approach I can take instead of using: ...

Does the message "The reference 'gridOptions' denotes a private component member in Angular" signify that I may not be adhering to recommended coding standards?

Utilizing ag-grid as a framework for grid development is my current approach. I have gone through a straightforward tutorial and here is the code I have so far: typography.component.html https://i.stack.imgur.com/XKjfY.png typography.component.ts i ...

A: TypeScript Error TS7006: Parameter implicitly has an 'any' type

TS7006: The parameter 'port' is implicitly assigned an 'any' type. constructor(port) { TS7006: The parameter 'message' is implicitly assigned an 'any' type. Emit(message) { I'm confused because all the other r ...

Having trouble with routerLink in your custom library while using Angular 4?

In my Angular 4 project, I have developed a custom sidebar library and integrated it into the main project. My current issue is that I want to provide the option for users to "open in new tab/window" from the browser's context menu without having the ...

Configuring VS Code's "tasks.json" file to compile all .ts files can be done by following these steps

Apologies if this question has been asked before, but could someone please redirect me to the relevant thread if so. I am wondering if it is possible to set up VS Code's "tasks.json" to automatically compile all .ts files within a folder. Currently, I ...

Struggling to translate JavaScript code into Typescript

Currently in the process of converting my JavaScript code to Typescript, and encountering an error while working on the routes page stating Binding element 'allowedRoles' implicitly has an 'any' type. ProtectedRoutes.tsx const Protecte ...

Creating a TypeScript function that utilizes generics to automatically infer the return type

How can I create a function with a generic argument that can return any type, and have the return type inferred from its usage? I attempted the following code: type Thing<T> = <U>(value: T) => U const shouldMakeStrings: Thing<string> ...

What is the process of assigning a value type to a generic key type of an object in typescript?

I am currently working on developing a function that can merge and sort pre-sorted arrays into one single sorted array. This function takes an array of arrays containing objects, along with a specified key for comparison purposes. It is important to ensure ...

An error has occurred in the Next.js App: createContext function is not defined

While developing a Next.js application, I keep encountering the same error message TypeError: (0 , react__WEBPACK_IMPORTED_MODULE_0__.createContext) is not a function every time I try to run my app using npm run dev. This issue arises when attempting to co ...

Maximizing the potential of NestJS apps with Docker

I am working on a NestJS project that consists of multiple apps structured as follows: my-project: -- apps; --- app-one ---- src ---- tsconfig.app.json --- app-two ---- src ---- tsconfig.app.json -- libs -- package.json -- etc... Within this project, I ha ...

Is importing all models into every component considered poor practice?

At my workplace, there is a practice in the legacy code where every single model is imported into all components, regardless of whether they are needed or not. For example: import * as models from '../../view-models/models' ....... let parrot: m ...

Tips for creating an input box that only accepts floating point numbers:

I have a custom component - a text box that I am using in two different places. In one location, it accepts integers and in another, floats. For the integer validation (where dataType=2), I have successfully implemented it. However, for the float validat ...

Tips for managing variables to display or hide in various components using Angular

In this example, there are 3 main components: The first component is A.component.ts: This is the parent component where an HTTP call is made to retrieve a response. const res = this.http.post("https://api.com/abcde", { test: true, }); res.subscribe((r ...

Tips on creating type definitions for CSS modules in Parcel?

As someone who is brand new to Parcel, I have a question that may seem naive. In my project, I am using typescript, react, less, and parcel. I am encountering an error with typescript stating 'Cannot find module 'xxx' or its corresponding t ...

Utilizing ngModel with an uninitialized object

What is the most effective way to populate an empty instance of a class with values? For example, I have a User Class and need to create a new user. In my component, I initialize an empty User Object "user: User;". The constructor sets some properties, w ...

The primary origin of TypeScript is derived from the compiled JavaScript and its corresponding source map

Being new to sourcemaps and typescript, I am faced with a project that has been compiled into a single javascript file from multiple typescript files. The files available to me are: lib.js (the compiled js code of the project) lib.js.map (the source map ...

Enhance your Vuex action types in Typescript by adding new actions or extending existing

I'm new to Typescript and I'm exploring ways to add specific type structure to all Actions declared in Vue store without repeating them in every Vuex module file. For instance, instead of manually defining types for each action in every store fi ...

What causes an ObjectUnsubscribedError to be triggered when removing and then re-adding a child component in Angular?

Within a parent component, there is a list: items: SomeType; The values of this list are obtained from a service: this.someService.items$.subscribe(items => { this.items = items; }); At some point, the list is updated with new criteria: this.some ...

Issue with Cypress TypeScript: Unable to locate @angular/core module in my React application

I am currently in the process of updating my Cypress version from 9.70 to 10.7.0. Although I have fixed almost all the bugs, I have encountered a strange message stating that @angular/core or its corresponding type declarations cannot be found. My applica ...