Incorrect inference of type in a generic function when using key and value as arguments

I am working with a MyMap type that has 2 keys, each taking an array of callbacks with different signatures.

type MyMap = {
   type1: (() => void)[] 
   type2: ((data: string) => void)[]
}

My goal is to create a generic function that can add a key and callback to an object of type MyMap.

const map: MyMap = { type1: [], type2: [] };

function addToMap<T extends keyof MyMap>(k: T,  cb: MyMap[T][number]): void {
        map[k].push(cb);
}

When attempting to push the callback into the array, TypeScript throws an error:

Argument of type '(() => void) | ((data: string) => void)' is not assignable to parameter of type '() => void'.
  Type '(data: string) => void' is not assignable to type '() => void'.
    Target signature provides too few arguments. Expected 1 or more, but got 0.(2345)

Is there a way to help TypeScript recognize that the provided callback matches the key?

Answer №1

When dealing with TypeScript and the MyMap, there is an issue regarding its ability to recognize that each member of MyMap with key K is actually an array that allows you to use push to add a value of type MyMap[K][number] to it. It can understand this for individual instances of K, but when K is generic, things get more complicated. This is because TypeScript struggles to automatically generalize a list of details into a singular concept. Therefore, instead of seeing map[k].push as one coherent method, it views it as a union of methods where any connection between map[k] and cb is lost.


To align your logic with the compiler's understanding, it is recommended to explicitly represent these operations in terms of generality. One approach outlined in microsoft/TypeScript#47109 involves starting from your existing MyMap type:

type MyMap = {
  type1: (() => void)[]
  type2: ((data: string) => void)[]
}
const map: MyMap = { type1: [], type2: [] };

From here, you create a "base" version of the type, focusing solely on the elements within the arrays:

type MyMapBase = { [K in keyof MyMap]: MyMap[K][number] }
    
/* type MyMapBase = {
    type1: () => void;
    type2: (data: string) => void;
} */

With this base type set up, you can define addToMap() as a generic function operating on MyMapBase:

function addToMap<K extends keyof MyMap>(k: K, cb: MyMapBase[K]): void {
  const m: { [K in keyof MyMap]: MyMapBase[K][] } = map;
  m[k].push(cb);
}

This approach involves assigning map to a variable m that adheres to the mapped type

{[K in keyof MyMap]: MyMapBase[K][]}
. By structuring it in this way, the compiler can verify each property individually, allowing for successful implementation. Now, m is explicitly written as a type that abstracts over MyMapBase generically, enabling easy addition of values based on their respective types.


It would be more straightforward if we initially worked with MyMapBase, as that is the underlying type being abstracted over. However, if you already have MyMap defined elsewhere, this code demonstrates how you can still derive the necessary components from it.

Code Playground

Answer №2

If you are confident that you will never encounter a scenario where the function is called with an incorrect key/cb pair type, such as

addToMap("type1", (data) =>...);
, you can easily resolve it using type assertion:

map[k].push(cb 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

Tips on inferring a distinct generic type for every element within an array of objects

I'm working on code that creates a timeline chart for data. I want the code to accept an array of series, each containing data and various getters used to position and render the data on the chart. Below is a simplified example of the code. The actua ...

Typescript feature: Configuring BaseUrl with nested directories

When utilizing the 'baseUrl' property along with the 'paths' property in this manner: "baseUrl": "./src", "paths": { "app-component": [ "app/app.component"], "test-component": [ "app/test/test.component" ] } the compilation proces ...

What is the most efficient approach to handle the next state after calling setState in react with immer?

Using Typescript, React, and Immer, I have set up a state management system to save profiles with multiple options. My goal is to ensure that the active profile has the correct option data before saving it. const { updateProfileList, getProfile } = useProf ...

Tips on integrating Cheerio with Angular framework

My Angular website is encountering errors in Google Dev Tools's console when I utilize a Cheerio method load('<h2 class="title">Hello world</h2>'); as per the instructions on the Github page. This application is fresh, and the s ...

The initial invocation of OidcSecurityService.getAccessToken() returns null as the token

Our internal application requires all users to be authenticated and authorized, including for the home page. To achieve this, we use an HttpInterceptor to add a bearer token to our API requests. Initially, when rendering the first set of data with the fir ...

Styling in Angular 2: Leveraging StyleUrls and Custom Styles

I've encountered an issue where I can't use both styleUrls and styles in a Component (whichever is declared last takes precedence). What's the best way to work around this? I'd like to use the ./board.component.css file for base styles ...

Exploring nested JSON objects within an array using ngFor directive

My application uses Angular 6 and Firebase. I am trying to showcase a list of all appointments. Below is my approach: service.ts getRDV() { this.rdvList = this.firebase.list('/rdv'); return this.rdvList; } Model: export class RDV { key: ...

Unable to refresh the context following a successful API call

My current project in NextJS requires a simple login function, and I have been attempting to implement it using the Context API to store user data. However, I am facing an issue where the context is not updating properly after fetching data from the back-e ...

Error: Import statement cannot be used outside a module (@cucumber/cucumber) while using Node.JS, Playwright, and Cucumber framework

I encountered an issue while attempting to compile my Node.js code that is compliant with ECMAScript 6: $ npx cucumber-js --require features/step_definitions/steps.ts --exit import { Before, Given, When, Then } from "@cucumber/cucumber"; ^^^^^^ ...

Is there a way for me to verify that the key of one object is a subset of the keys of another object?

export const masterKeysObject = { MAIN: 'main', REDIRECT: 'redirect', DASHBOARD: 'dashboard', USER_ID_PARAM: ':userId', CREATE_NEW: 'create_new' } as const; type MasterKeys = keyof type ...

An object in typescript has the potential to be undefined

Just starting out with Typescript and hitting a snag. Can't seem to resolve this error and struggling to find the right solution useAudio.tsx import { useEffect, useRef } from 'react'; type Options = { volume: number; playbackRate: num ...

What is the process for integrating an extension function into an Express response using TypeScript?

I am looking to enhance the Response object in Express by adding custom functions. Specifically, I want to introduce a function: sendError(statusCode: number, errorMessage: string) which can be called from anywhere like this: response.sendError(500, &qu ...

Issue with mui TextField label width not adjusting properly with font override

Whenever I change the font of the label, the width of the label does not adjust accordingly and the text appears to be outlined. For a demonstration, you can check out this example on CodeSandbox ...

Instructions on how to extract and display the key-value pairs from a nested JSON array in Angular 2, then populate them in a select box

My goal is to extract data from a Json data model and populate a select box with the data as drop-down items. However, I am encountering difficulties in achieving this as the entire Json array is being printed instead of just the 3 major headings that I wa ...

There is no such member found in the component declaration, template variable declarations, or element references

Seeking help for a simple fix. The objective is to have the element slide out from the top of the page upon hover. The code is functioning correctly, but I am encountering an error. Error: [Angular] Identifier 'compartmentOpen' is not defined. ...

Unable to bring in personalized typescript package in node.js

After struggling for hours trying to figure it out on my own, I finally decided to seek help on stackoverflow. I have a custom typescript project/package that I want to use with zx. I packed it using npm pack and installed the tar.gz globally. However, whe ...

Encountered Runtime Issue of TypeError: undefined cannot be iterated (unable to access property Symbol(Symbol.iterator))

I've been working on a multiselect feature in my Next.js project, utilizing states and setting them accordingly. However, I keep encountering this specific error: https://i.stack.imgur.com/m8zuP.png whenever I click on this: https://i.stack.imgur.c ...

When invoking a function within another function, it is essential to ensure that both values are returned within the function

When calling a function within another function function1(){ return this.a } function2(){ return this.b } To output in string format, you can call function1 inside function2 function2(){ var resultOfFunction1 = this.function1(); return resultOfFunction1 ...

How to showcase the date in a unique format using Angular

Does anyone know of a JavaScript ES7 method that can convert the output of new Date() into the format shown below? If there isn't a built-in method, I am willing to manually parse or find/replace it myself. 2020-06-30 07.49.28 I would like the da ...

Why does Angular CLI create "spec.ts" files?

My current go-to tool for generating and serving projects is Angular CLI. So far, it's been pretty smooth sailing – although I've noticed that for my smaller learning projects, it tends to create more than what I actually need. But hey, that&ap ...