The world of TypeScript generics and derived data types

I'm looking to streamline the process of calling multiple functions by creating a function that handles this task. These functions all require similar business logic and cleanup operations.

function foo(arg: number) {
    // perform actions using arg
}

function bar(arg: string) {
    // perform actions using arg
}

const FUNCTIONS = {
    foo,
    bar,
} as const;

type FnType = keyof typeof FUNCTIONS;

type FnArg<Type extends FnType> = Parameters<typeof FUNCTIONS[Type]>[0];

function callFunction<Type extends FnType>(type: Type, arg: FnArg<Type>) {
    // shared business logic for all functions

    const fn = FUNCTIONS[type];

    return fn(arg); 
}

// Using generics ensures arguments are checked for correctness
callFunction('foo', 5);
callFunction('foo', 'arg'); 

callFunction('bar', 'arg');
callFunction('bar', 5); 

While I've implemented generics to enforce argument type checking, TypeScript doesn't seem to infer this when actually invoking the functions. Is there a way to make TypeScript understand that fn(arg) will work correctly?

Answer №1

Upon inspection, it is evident that the compiler faces challenges in identifying the relationship between the types of FUNCTIONS[type] and arg. This issue mirrors the one outlined in microsoft/TypeScript#30581, where it revolves around correlated union types. The problem arises when the compiler attempts to invoke FUNCTIONS[type](arg), causing it to widen the generic type of FUNCTIONS[type] to the union

((arg: string) => void) | (arg: number) => void))
, and the type of arg to string | number. Unfortunately, calling a function with an argument of a conflicting type results in errors as inferred by the compiler, making it impossible to execute smoothly.

function callFunction<K extends keyof FnArg>(type: K, arg: FnArg[K]) {
  const fn = FUNCTIONS[type];
  // const fn: (arg: never) => void
  // (parameter) arg: string | number
  return fn(arg); // error! 😢
}

However, a solution exists for this dilemma, detailed in microsoft/TypeScript#47109. The key lies in refining your types to be explicitly defined as actions on generic indexed accesses within mapped types over a base object type. This strategy aims to establish fn as having the generic type (arg: XXX) => void while ensuring arg corresponds to the same generic XXX. Therefore, adapting the type of

FUNCTIONS</code to map over the base type is imperative.</p>
<p>To accomplish this, you can follow these steps. Start by renaming the <code>FUNCTIONS
variable:

const _FUNCTIONS = {
  foo,
  bar,
} as const;

Subsequently, construct the base object type using the renamed variable:

type FnArg = { [K in keyof typeof _FUNCTIONS]:
  Parameters<typeof _FUNCTIONS[K]>[0] }

/* type FnArg = {
    readonly foo: number;
    readonly bar: string;
} */

Declare FUNCTIONS as a mapped type over FnArg:

const FUNCTIONS: {
  [K in keyof FnArg]: (arg: FnArg[K]) => void
} = _FUNCTIONS;

With these adjustments, you can utilize callFunction() to operate on generic indexed accesses efficiently:

function callFunction<K extends keyof FnArg>(type: K, arg: FnArg[K]) {
  const fn = FUNCTIONS[type];
  // const fn: (arg: FnArg[K]) => void
  // (parameter) arg: FnArg[K]
  return fn(arg); // okay
}

This method resolves the previous hurdles admirably!


Note that the equivalency between the types of FUNCTIONS and

_FUNCTIONS</code becomes apparent if examined closely:</p>
<pre><code>/* const _FUNCTIONS: {
    readonly foo: (arg: number) => void;
    readonly bar: (arg: string) => void;
} */

/* const FUNCTIONS: {
    readonly foo: (arg: number) => void;
    readonly bar: (arg: string) => void;
} */

The only distinction lies in their internal representation, where the mapped type of

FUNCTIONS</code allows the compiler to establish the correlation between <code>FUNCTIONS[type]
and arg. Swapping FUNCTIONS back to
_FUNCTIONS</code within <code>callFuction()
's body would reintroduce the initial error.

Overall, this challenge showcases a nuanced obstacle that has been expertly navigated through the approach highlighted in microsoft/TypeScript#47109, elucidating its functionality and importance.

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

Looking for the final entry in a table using AngularJS

Hey everyone, I'm dealing with a table row situation here <tbody> <tr *ngFor="let data of List | paginate : { itemsPerPage: 10, currentPage: p }; let i = index"> <td>{{ d ...

The 'connectedCallback' property is not found in the 'HTMLElement' type

After taking a break from my project for a year, I came back to find that certain code which used to work is now causing issues: interface HTMLElement { attributeChangedCallback(attributeName: string, oldValue: string, newValue: string): void; con ...

Guide on converting a JSON object into a TypeScript Object

I'm currently having issues converting my JSON Object into a TypeScript class with matching attributes. Can someone help me identify what I'm doing wrong? Employee Class export class Employee{ firstname: string; lastname: string; bi ...

Is foreach not iterating through the elements properly?

In my code, I have a loop on rxDetails that is supposed to add a new field payAmount if any rxNumber matches with the data. However, when I run the forEach loop as shown below, it always misses the rxNumber 15131503 in the return. I'm not sure what I ...

What is the proper way to compare enum values using the greater than operator?

Here is an example enum: enum Status { inactive = -1, active = 0, pending = 1, processing = 2, completed = 3, } I am trying to compare values using the greater than operator in a condition. However, the current comparison always results in false ...

Can anyone assist me with creating a custom sorting pipe in Angular 2?

*ngFor="let match of virtual | groupby : 'gameid' I have this code snippet that uses a pipe to group by the 'gameid' field, which consists of numbers like 23342341. Now, I need help sorting this array in ascending order based on the g ...

Using JavaScript or TypeScript to locate the key and add the value to an array

My dilemma involves an object structured as follows: [{ Date: 01/11/2022, Questionnaire: [ {Title: 'Rating', Ans: '5' }, {Title: 'Comment', Ans: 'Awesome' } ] }, { Date: 01/11/2022, Questionnaire ...

How can you utilize both defineProps with TypeScript and set default values in Vue 3 setup? (Typescript)

Is there a way to use TypeScript types and default values in the "defineProps" function? I'm having difficulty getting it to work. Code snippet: const props = defineProps<{ type?: string color?: 'color-primary' | 'color-danger&a ...

The error thrown by Mongoose, stating "TypeError: Cannot read property 'catch' of undefined," is not caught after the data is saved

After updating to Mongoose version 5.0.15, I encountered an error that says TypeError: Cannot read property 'catch' of undefined when trying to save my object with errors found, even though everything was working fine on Mongoose version 4.13.11. ...

How can one overcome CORS policies to retrieve the title of a webpage using JavaScript?

As I work on a plugin for Obsidian that expands shortened urls like bit.ly or t.co to their full-length versions in Markdown, I encounter a problem. I need to fetch the page title in order to properly create a Markdown link [title](web link). Unfortunatel ...

Angular - How to fix the issue of Async pipe not updating the View after AfterViewInit emits a new value

I have a straightforward component that contains a BehaviorSubject. Within my template, I utilize the async pipe to display the most recent value from the BehaviorSubject. When the value is emitted during the OnInit lifecycle hook, the view updates correc ...

Tips for setting up a React TypeScript project with custom folder paths, such as being able to access components with `@components/` included

I'm looking to streamline the relative url imports for my React TypeScript project. Instead of using something messy like ../../../contexts/AuthContext, I want to simplify it to just @contexts/AuthContexts. I attempted to update my tsconfig.json with ...

The CSS styles are functioning correctly in index.html, but they are not applying properly in the component.html

When the UI Element is clicked, it should add the class "open" to the list item (li), causing it to open in a collapsed state. However, this functionality does not seem to be working in the xxx.component.html file. Screenshot [] ...

A step-by-step guide to showcasing dates in HTML with Angular

I have set up two datepickers in my HTML file using bootstrap and I am attempting to display a message that shows the period between the first selected date and the second selected date. The typescript class is as follows: export class Datepicker { ...

Intellisense capabilities within the Gruntfile.js

Is it a feasible option to enable intellisense functionality within a Gruntfile? Given that 'grunt' is not defined globally but serves as a parameter in the Gruntfile, VSCode may interpret it as an unspecified function parameter 'any'. ...

Angular2 Uniqueness Validator: Ensuring Data Integrity

Within my Angular2 form field, I am trying to ensure that the inputted value does not already exist. The challenge lies in accessing instance members within my custom validator function, codeUnique(). Upon execution, "this" refers to either FormControl o ...

"X is not compatible with these types of property," but it is not the case

I attempted to instantiate an interface object with properties initialized from another object as follows: id: data.reference.id Even though the properties are compatible, the TypeScript compiler is throwing an error. I am confused about why this is happ ...

Pinia is having trouble importing the named export 'computed' from a non-ECMAScript module. Only the default export is accessible in this case

Having trouble using Pinia in Nuxt 2.15.8 and Vue 2.7.10 with Typescript. I've tried numerous methods and installed various dependencies, but nothing seems to work. After exhausting all options, I even had to restart my main folders on GitHub. The dep ...

Utilizing Office.js: Incorporating Angular CLI to Call a Function in a Generated Function-File

After using angular-cli to create a new project, I integrated ng-office-ui-fabric and its dependencies. I included in index.html, added polyfills to angular.json, and everything seemed to be working smoothly. When testing the add-in in Word, the taskpane ...

What is the best way to test a callback function of a React component that is encapsulated with withRouter()?

In my Jest and Enzyme testing of a TypeScript-written React project, I have a component wrapped in a React-Router router. Here's a snippet of the code: import { SomeButton } from './someButton'; import { RouteComponentProps, withRouter } fr ...