There are no call signatures available for the unspecified type when attempting to extract callable keys from a union

When attempting to write a legacy function within our codebase that invokes methods on certain objects while also handling errors, I encountered difficulty involving the accuracy of the return type. The existing solution outlined below is effective at constraining the action type but falls short in providing a precise return type (defaults to any).

The main goal of the entityGetter function is to accept either entityA or entityB and only callable keys that start with 'get'. This specific aspect works as expected, indicating that achieving a correct return value should be feasible; yet the issue lies in why the action does not have the appropriate type when accessing entity.

I've experimented with setting defaults for T and A, which unfortunately did not yield any changes.

Is it viable to address this without modifying the (runtime) interface of the entityGetter function? Ideally, I'd like to avoid making JavaScript alterations altogether, although implementing changes within the function's body would be acceptable.

type TEntityA = {
  getA: () => string;
  getB: () => string;
  setC: (arg0: string) => string;
  valD: number;
  getE: () => object;
};
type TEntityB = {
  getA: () => string;
  getB: () => number;
  setC: (arg0: string) => string;
  valD: string;
};
type TEntities = TEntityA | TEntityB;

// type KeysOfType = https://stackoverflow.com/a/64254114

type TGetterKeys<T extends TEntities> = keyof Pick<T, keyof T & (`get${string}`)>;
type TCallableKeys<T extends TEntities> = KeysOfType<T, () => unknown>;
type TGetters<T extends TEntities> = TGetterKeys<T> & TCallableKeys<T>;

const entityGetter = <
  T extends TEntities,
  A extends TGetters<T>
>(action: A, entity: T) => entity[action]();
// This expression is not callable. Type 'unknown' has no call signatures.


const entityA = new EntityA();
const entityB = new EntityB();

const AgetA = entityGetter('getA', entityA); // => string
const BgetB = entityGetter('getB', entityB); // => number

const AgetX = entityGetter('getX', entityA);
// Argument of type '"getX"' is not assignable to parameter of type '"getA" | "getB" | "getE"'.

Playground

Answer №1

TS often struggles to grasp the fact that T[A] always represents some form of a function. Therefore, our first task is to convince TS with this expression: (entity[action] as Function)(), ensuring it understands that this is callable. However, since ReturnType<Function> is any, we must explicitly define the ReturnType for entityGetter.

In a similar vein, a straightforward ReturnType<T[A]> doesn't cut it either. TS cannot anticipate (in this generic scenario, prior to being called with specific values and types) that T[A] is indeed callable, hence it refuses to proceed in such cases. Therefore, manual intervention is necessary here:

T[A] extends () => infer R? R: never
. This provides predictability for TS.

I'd also introduce an overview of TGetters.

type TGetters<T> = {
  [K in keyof T]: T[K & `${"get" | "is"}${string}`] extends (...args: any) => any ? K : never;
}[keyof T];

const entityGetter = <
  T extends TEntities,
  A extends TGetters<T>
>(action: A, entity: T): T[A] extends () => infer R ? R : never =>
  (entity[action] as Function)();

or

type KeysOfType<T, U> = keyof T & keyof {
  [K in keyof T as T[K] extends U ? K : never]: 1
};

const entityGetter = <
  T extends TEntities,
  A extends KeysOfType<T, () => unknown> & `${"get" | "is"}${string}`
>(action: A, entity: T): T[A] extends () => infer R ? R : never =>
  (entity[action] as Function)();

TS Playground

Side Note: T extends TEntities serves as a safeguard, restricting you from using this function with any object other than TEntityA | TEntityB, even if they adhere to the general pattern of having getter functions starting with get or is. To facilitate typing, simply using T would suffice.

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

How can I efficiently iterate through the array of index IDs and then iterate individually through the communes, categories, and locations?

Currently, I am developing a nodejs typescript API where I am retrieving an array of objects using a map loop. The data for "communes", "category", and "location" is fetched from another API function based on the issuerId. However, I am facing issues with ...

Differences in weekend start and end days vary across cultures

Looking for a solution to determine the weekend days per culture code in Typescript/Javascript? While most countries have weekends on Sat-Sun, there are exceptions like Mexico (only Sunday) and some middle-eastern countries (Fri-Sat). It would be helpful ...

The TypeScript Promise error codes TS2304 and TS2529 are causing confusion among

I came across the code below: function asyncTask(): Promise<string> { return new Promise<string>(resolve => resolve); } This code resulted in the following error: TS2304: cannot find name 'Promise' To address this issue, ...

Having trouble importing SVG as a React component in Next.js

I initially developed a project using create-react-app with Typescript, but later I was tasked with integrating next.js into it. Unfortunately, this caused some SVGs throughout the application to break. To resolve this issue, I implemented the following pa ...

rxjs "switch to" once the expansion is complete

I am currently working on the following code snippet: const outputFile = fs.createWriteStream(outputPath); const requisitionData = this.login().pipe( map(response => response.data.token), switchMap(loginToken => this.getRequisitions( ...

Utilizing the useState hook with generics in React for efficient data management

Utilizing a unique react hook designed to manage input validation for text fields and checkboxes, adaptable to both string and boolean values through the use of generics. An error is encountered when attempting to assign a value using setValue, displaying ...

Adding date restrictions to the main method is necessary for controlling the input and output

I have a function in my react-native package that requires "from" and "to" dates as parameters. I need to implement a rule that ensures the "to" date is always after the "from" date. Where should I insert this requirement in the following code snippe ...

Leverage the new Animation support in RC 5 to animate each item in an *ngFor list sequentially in Angular 2

I have a unique component that retrieves a list of items from the server and then displays that list using *ngFor in the template. My goal is to add animation to the list display, with each item animating in one after the other. I am experimenting with t ...

TypeScript combined with Vue 3: Uncaught ReferenceError - variable has not been declared

At the start of my <script>, I define a variable with type any. Later on, within the same script, I reference this variable in one of my methods. Strangely, although my IDE does not raise any complaints, a runtime error occurs in my console: Referenc ...

Tips for updating a JSON object value in Node.js

Storing a JSON object in a JSON file is important for passing data during an API call. To update the object, replace "it-goes-here" with the following {} block. Newly updated data: { "parenturl":"xxx.com", "user ...

Issue with exporting member in VS Code but not in console

I currently have a NestJS project (project A) with one module that utilizes the @nestjs/typeorm and typeorm packages. In my other NestJS project (project B), I plan to use this module. However, there is a potential issue. Project B contains entities that ...

Executing an AngularJS function using regular JavaScript code

I am currently working with AngularJS and Typescript. I have integrated an external library into my project and now I need to call an AngularJS method from that library, which is written in vanilla JavaScript. I tried following this example, but unfortunat ...

What causes a union with a conditionally assigned property to lead to more relaxed types than anticipated?

Take a look at this TypeScript code snippet: const test = Math.random() < 0.5 ? { a: 1, b: 2 } : {}; Based on the code above, I would assume the type of object 'test' to be: const test: { a: number; b: number; } | {} This is the most str ...

Parameters in Typescript decorators

Can someone help me understand the various parameters of a Typescript decorator? function myDecorator(target) { // do something with 'target' ... } In the given example, I am aware that 'target' represents the function/class to wh ...

Error encountered when attempting to utilize ngTemplate to embed filter within a table

I am facing an issue with a component that includes a primeng table. Within a table row, I have an ng-container to project a p-columnFilter into the table from an external source. However, when trying to pass the filter into the template, I encounter a Nul ...

Beautiful ExpressionChangedAfterItHasBeenCheckedError

I need your help Input field where I can enter a Student email or IAM, which will be added to a string array List displaying all the students I have added, using a for loop as shown below Delete button to remove a student from the array The list has a sp ...

What is the reason behind TypeScript's lack of inference for function parameter types when they are passed to a typed function?

Check out the code snippets below: function functionA(x: string, y: number, z: SpecialType): void { } const functionWrapper: (x, y, z) => functionA(x, y, z); The parameters of functionWrapper are currently assigned the type any. Is there a way we can ...

Prisma - Modify a single resource with additional criteria

Is it feasible to update a resource under multiple conditions? Consider the tables below: +----------+----------+ | Table1 | Table2 | +----------+----------+ | id | id | | param1T1 | param1T2 | | param2T1 | param2T2 | | idTable2 | ...

Enhance your TypeScript React development with NeoVim

As a newcomer to vim, I decided to test my configuration with react and typescript. To do this, I created a simple demo app using the command npx create-react-app demo --template typescript. Next, I opened the directory in neovim by running nvim .. However ...

Integrating one service into another allows for seamless access to the imported service's properties and methods within an Angular

After reviewing the content at https://angular.io/guide/dependency-injection-in-action, it appears that what I am trying to achieve should be feasible. However, I encounter this error when attempting to invoke a method on my injected service from another s ...