Is it possible to create a map of functions that preserves parameter types? How can variadic tuple types in TypeScript v4 potentially enhance this

Initially, I faced a challenge when trying to implement a function similar to mapDispatchToProps in Redux. I struggled with handling an array of functions (action creators) as arguments, but managed to come up with a workaround that works, although it feels hacky. I'm curious if there's a better way to approach this.

The issue I encountered was that the return type didn't preserve types for each item; instead, it generalized them into a union.

Update: the problem lies in maintaining parameter types for each function within the resulting array.

In essence, the code below should have no errors:

type F = (...x: any[]) => any
    type Wrap<T extends F> = (...x: Parameters<T>) => void

    const wrap = (fn: any) => (...a: any) => { fn(...a)}

    // current solution that seems to work
    // issue is with K being too wide (number | string | symbol) for array index which causes suppression
    // // @ts-expect-error
    // function main<Fs extends readonly F[]>(fs: Fs): {[K in keyof Fs]: Wrap<Fs[K>]}

    // TODO: desired solution, not yet completed: every item in `fs` should be wrapped with `Wrap`
    function main<Fs extends readonly F[]>(fs: Fs): [...Fs]

    function main(fs: any) { return fs.map(wrap) }

    const n = (x: number) => x
    const s = (x: string) => x
    const fs = main([n, s] as const)

    // TESTING PARAMETER TYPES
    fs[0](1)
    fs[1]('1')
    // @ts-expect-error
    fs[0]('1')
    // @ts-expect-error
    fs[1](1)

    // TESTING RETURN TYPES
    const _1: void = fs[0](1)
    const _2: void = fs[1]('1')
    // @ts-expect-error
    const _3: number = fs[0](1)
    // @ts-expect-error
    const _4: string = fs[1]('1')
    

TypeScript playground

P.S: There is an open GitHub issue related to the problem with my initial solution (#1) as of August 25, 2020. It pertains to the width of `keyof ArrayType`, rather than variadic tuple types.

Answer №1

TypeScript 4.0 allows for the representation of variadic tuple types which can be utilized to annotate the main function and specify its functionality like so:

const main = <F extends any[]>(f: F): [...F] => [...f];

In TypeScript versions 3.9 and below, a similar behavior can be achieved by asserting that the return type is the same as the input type, as shown in this example:

const main = <F extends any[]>(f: F) => [...f] as F;

By providing hints such as using tuple destructuring or adding tuple type constraints to the function signature, you can ensure that the compiler infers the correct tuple types based on your intention.

Ultimately, blending these techniques together should help resolve any issues you may encounter with type inference.

Answer №2

TS4 variadic tuples are not necessary at all. Utilize mapped (tuple) types instead. This code snippet is functional in TypeScript 3.5:

function main<Fs extends readonly F[]>(fs: Fs):
    { [K in keyof Fs]: Fs[K] extends F ? Wrap<Fs[K]> : never }

Playground Link


Update: If using array types instead of tuple types, the code will return an array type where individual indices are no longer identifiable. To retain tuple information, use as const:

const test3 = [
    console.log,
    (x: number) => x,
] as const;
const fs3 = main(test3);
// const fs3: readonly [Wrap<(...data: any[]) => void>, Wrap<(x: number) => number>]

If you want to enforce the preservation of indices and allow only tuples, utilize this workaround:

function main<Fs extends readonly F[]>(
    fs: number extends Fs['length'] ? never : Fs):
    { [K in keyof Fs]: Fs[K] extends F ? Wrap<Fs[K]> : never }

(edit:

K in Exclude<keyof Fs, keyof []>
-> K in keyof Fs like above, not sure why I had that)
This solution is compatible with TS3.

Playground Link

Answer №3

To make TypeScript infer the tuple type, you should delve into Variadic Tuples. It seems like you have to restrict the generic type parameter to any[] and then return a spread of that parameter:

type Main = <T extends any[]>(fns: T) => [...T]

const main: Main = (fns) => [...fns]

Once you do this, everything will work as expected.

Check it out on Playground

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

Create a configuration featuring filter options similar to Notion's functionality

The objective is to create a system that can establish certain constraints, similar to the way Notion handles filter properties. https://i.sstatic.net/plctW.png System A sets up the constraints and System C evaluates them, both using Typescript. However, ...

Steps to prevent subfolder imports in my npm package

My npm package is built using: typescript webpack webpack.config: {... entry: './src/index.ts } library tree: - package.json - src - - index.ts - - ...all_my_code... I have all my library functionality and types exported from the index.ts file. T ...

Efficient Ways to Utilize Global CSS in an Angular Project Without CLI

I am utilizing ASP.NET MVC as the server and Angular as the client application. Instead of a static index.html file, I have index.cshtml. The styles I am using are global styles rather than component-scoped. My query revolves around working with a bunch ...

Using React to implement MUI autocomplete feature alongside a MUI form

Recently, I have been utilizing a MUI form structured in the following manner: <Box component="form" onSubmit={event => { return handleSubmit(event); }} noValidate sx={{mt: 1}}> <TextField margin="normal" ...

Generate several invoices with just a single click using TypeScript

I'm interested in efficiently printing multiple custom HTML invoices with just one click, similar to this example: Although I attempted to achieve this functionality using the following method, it appears to be incorrect as it prompts the print dialo ...

What are the new features for listening to events in Vue 3?

Currently, I am manually triggering an event: const emit = defineEmits<{ (e: 'update:modelValue', value: string | number): void }>() // [..] <input type="text" :value="modelValue" @input="emit(&apos ...

Strange occurrences observed while looping through an enum in TypeScript

Just now, I came across this issue while attempting to loop through an enum. Imagine you have the following: enum Gender { Male = 1, Female = 2 } If you write: for (let gender in Gender) { console.log(gender) } You will notice that it iter ...

The creation of a fresh child instance in Typescript using rest parameters

My goal is to create a parent class that allows children to generate new instances of the same child type. When I specify the number of parameters, everything functions correctly: abstract class AClass { protected sameTypeWithSingle ( x: any ): t ...

Supporting right-to-left (RTL) localization in Angular 2 and later versions

When it comes to incorporating right-to-left (RTL) support into a localized Angular 2+ application, particularly for languages like Hebrew and Arabic, what is considered the best approach? I have explored various tutorials, including Internationalization ...

The element is implicitly assigned an 'any' type due to the fact that an expression of type 'any' cannot be used to index types in nodejs and solidity

I am in need of setting networks in my contract using NodeJS and TypeScript. Below is the code I have written: let networkId: any = await global.web3.eth.net.getId(); let tetherData = await Tether.networks[networkId]; Unfortunately, I encountered ...

Instantiate a fresh object using the new keyword followed by the Class constructor, and if desired,

I'm looking for a class that I can easily create new instances from and optionally assign properties using the constructor. For instance: class Person { name: string; age: number; constructor(props: {name?: string, age?: number}) { this.nam ...

What could be causing the issue with my output not displaying correctly?

Hey guys! I'm working on creating a to-do list but I've encountered a problem. Whenever I enter a value in the text field, it doesn't get added to the array of list elements. Strangely enough, when I console.log it, it seems to work. Can any ...

Obtain the content of a clicked item on the following page using NextJs

I am currently working on a nextjs app that displays a list of 10 movies on the homepage, each with a Button / Link that leads to a specific page for that movie where all its content is shown. Initially, I tried adding the movie id to the Link like this: ...

Issue regarding angularjs type definitions

I am facing an issue with installing typings for Angular and I need some guidance on how to resolve the error. Any suggestions or assistance would be greatly appreciated! Below is the error message that I encountered: ERROR in C:\Users\test&b ...

angular table cell with a show more/less button

I'm trying to create a button that can hide/unhide text in a table cell if the length is greater than a certain number. However, the current implementation is not working as expected. The button ends up opening all texts in every cell, and it only wor ...

utilize a modal button in Angular to showcase images

I am working on a project where I want to display images upon clicking a button. How can I set up the openModal() method to achieve this functionality? The images will be fetched from the assets directory and will change depending on the choice made (B1, ...

Click the link to copy it and then paste the hyperlink

I am facing an issue with copying and pasting file names as hyperlinks. I have checkboxes next to multiple files and a share button. When I check the boxes and click on the share button, I want to copy the download URLs for those files. Although I can succ ...

Retrieving information from Next.js and Typescript with the help of getStaticProps

I've been working on a personal project with Next.js and TypeScript. I'm attempting to fetch data from an API and then map the items, but I'm running into issues. When I use console.log, it returns undefined. The file is located in the pages ...

In TypeScript, it can be challenging to determine the equality between a value and an enum

I am encountering an issue with my simple code: enum Color { BLUE, RED } class Brush { color: Color constructor(values) { this.color = values.color } } let JSON_RESPONSE = `{"color": "BLUE"}` let brush = new Brush(JSON.parse(JSON ...

What is the best way to add an external .js file to my Angular 2 application?

I'm currently working on a project using Angular 2's TypeScript API along with webpack to develop a web application. However, I've encountered an issue where one of my components needs to utilize functions from an external .js file that is p ...