Typescript's Return Type of Lookup Types

I have a scenario where I am working with a type containing function fields, along with a utility function to retrieve the function of that type based on its name:

type Person = {
  getAge: () => number;
  getName: () => string;
};

function getPersonFunction<K extends keyof Person>(person: Person, key: K): Person[K] {
  return person[key];
}

Now, I am looking to create another utility function that can dynamically execute these functions:

function callPersonFunction<K extends keyof Person>(person: Person, key: K) {
  return person[key]();
}

However, TypeScript is inferring the return type of this function as any. I attempted to use ReturnType<Person[K]> to define the return type explicitly, but it resulted in an error.

Is there a way for me to correctly invoke the function, dynamically infer its return type, and maintain generality?

Updated: Additional criteria:

  • Discounting only methods (credit to @Alex)
  • Methods involving input parameters

Answer №1

To achieve this transformation, utilize a mapped type that converts each property of the Attributes type into a function that retrieves the property's type.

By implementing this approach, you can define a type called Person where each attribute is represented as a method that outputs the corresponding value.

type Attributes = {
    age: number;
    name: string;
};

type Person = {
    [Key in keyof Attributes]: () => Attributes[Key];
};

function executePersonFunction<Key extends keyof Person>(actions: Person, key: Key) {
    return actions[key]();
}

const person: Person = {
    age: () => 30,
    name: () => "Alice"
};

const personAge = executePersonFunction(person, 'age')
const personName = executePersonFunction(person, 'name')

console.log("Person's Age:", personAge)
console.log("Person's Name:", personName)

Answer №2

The main issue at hand is TypeScript's inability to examine an object type such as Person and confirm or deduce a higher-level relationship like "for any K extends keyof Person, if Person[K] is a function, then Person[K] should be equivalent to

(...args: Parameters<Person[K]>) => ReturnType<Person[K]>
. This concept may appear straightforward to you, but TypeScript fails to grasp what Parameters<F> and ReturnType<F> signify for the generic types F. It can analyze these types for specific instances of F or K, but when it comes to the abstract generic scenario, TypeScript falters.

The solution lies in reorganizing your types so that the desired relationship is explicitly articulated within the type itself by using mapped types and leveraging generic indexes into those types. A comprehensive guide on this approach is outlined in microsoft/TypeScript#47109.


To illustrate with the provided example, you need to transform Person into a mapped type based on a 'base' interface:

interface PersonRet {
    getAge: number;
    getName: string;
}

type Person = { [K in keyof PersonRet]: () => PersonRet[K] }

Subsequently, your callPersonFunction will seamlessly function since its return type is automatically inferred as PersonRet[K]:

function callPersonFunction<K extends keyof Person>(person: Person, key: K) {
    return person[key]();
}

For scenarios where functions have input parameters and not all properties are functions, you can devise utility types to convert your type into fundamental parameter type and return type mappings:

type ParamMap<T> = { [K in keyof T as T[K] extends (...args: any) => any ? K : never]: 
  T[K] extends (...args: infer A) => any ? A : never }
type ReturnMap<T> = { [K in keyof T as T[K] extends (...args: any) => any ? K : never]: 
  T[K] extends (...args: any) => infer R ? R : never }

Upon implementation, evaluate these foundational mapping types:

class Foo {
    a: string = ""
    b: number = 123;
    c() { return this.a }
    d(x: number) { return this.b + x }
}

type FooParams = ParamMap<Foo>
// type FooParams = { c: []; d: [x: number]; }
type FooReturn = ReturnMap<Foo>
// type FooReturn = { c: string; d: number; }

Notice that only keys associated with methods are included, and the values represent parameter list types and return types, correspondingly. Hence, you can redefine Foo as a mapped type solely containing the methods:

type FooMethods = { [K in keyof FooParams]:
    (...args: FooParams[K]) => FooReturn[K] };
/* type FooMethods = {
    c: () => string;
    d: (x: number) => number;
} */

The FooMethods type supersedes Foo, explicitly portrayed as a mapped type over FooParams and FooReturn. Consequently, your generic calling function appears as follows:

function callFooFunction<K extends keyof FooParams>(
  foo: Foo, k: K, ...args: FooParams[K]
): FooReturn[K] {
    const fooMethods: FooMethods = foo;
    return fooMethods[k](...args)
}

This operates effectively as we deliberately broaden foo from Foo to FooMethods. By indexing into FooMethods with K, we acquire the generic single function type

(...args: FooParams[K]) => FooReturn[K]
, making it callable with an argument list of type FooParams[K] and yielding FooReturn[K] as intended.

You can verify its functionality as advertised:

callFooFunction(new Foo(), "c").toUpperCase()
callFooFunction(new Foo(), "d", 123).toFixed()

Results seem promising.

Visit Playground link for 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

Vue.js - A dynamic parent component generates content based on data passed from a renderless child component

I am currently working on developing a system for generating buttons using vue 3 and vue-class-component. The main goal is to create a flexible button generation process, where the number of buttons generated can vary (it could be just one or multiple). Us ...

Is there a way to access variables stored within a Singleton object?

I'm currently working on creating a singleton to retrieve two variables from different components. These variables are defined in a component that always runs before the others. The issue I'm facing is that the Singleton instance isn't bein ...

The art of transforming properties into boolean values (in-depth)

I need to convert all types to either boolean or object type CastDeep<T, K = boolean> = { [P in keyof T]: K extends K[] ? K[] : T[P] extends ReadonlyArray<K> ? ReadonlyArray<CastDeep<K>> : CastDeep<T[P]> ...

Angular 2 - Error: Regular expression missing forward slash syntax

Recently, I began working on an Angular 2 tutorial app using this repository. While I can successfully launch the app and display static content, I am facing challenges with rendering dynamic content from the component. I have a feeling that the error migh ...

Tips on exporting a TypeScript class while maintaining private related variables

Consider this scenario: const hidden = Symbol() export class Foo { static [hidden] = 'I prefer no one to modify this' } The compiler throws an error TS4028: Public static property '[hidden]' of exported class has or is using privat ...

Guidance on installing only TypeScript dependencies for building from package.json using npm, ensuring a leaner build without unnecessary 150MB of additional dependencies

Is there a way to optimize the dependency installation process for building, minimizing unnecessary packages and reducing the total download size by avoiding 150MB of excess files? This is more of a query rather than an immediate requirement Current depe ...

Errors related to TypeScript syntax have been detected within the node_modules/discord.js/typings/index.d.ts file for Discord.JS

I keep encountering typescript syntax errors after pulling from my git repository, updating all npm modules on the server, and running the start script. The errors persist even when using npm run dev or npx tsc. I've attempted the following troublesh ...

When running `ng serve` or `ng build --prod`, the dist folder is not created in an Angular 4 application

I recently completed building an Angular 4 app using angular-cli version 1.0.4 and generated the production build with the command ng build --prod. However, I encountered a problem as the expected dist folder was not created after executing this command. ...

Convert the generic primitive type to a string

Hello, I am trying to create a function that can determine the primitive type of an array. However, I am facing an issue and haven't been able to find a solution that fits my problem. Below is the function I have written: export function isGenericType ...

Error message: WebStorm shows that the argument type {providedIn: "root"} cannot be assigned to the parameter type {providedIn: Type<any> | "root" | null} and InjectableProvider

Transitioning my app from Angular v5 to v6 has presented me with a TypeScript error when trying to define providedIn in my providers. The argument type {providedIn: "root"} cannot be assigned to the parameter type {providedIn: Type | "root" | null} & ...

Choose a date range from the date picker

I am currently attempting to combine two dates using a rangepicker. Below is the command used to select the date: Cypress.Commands.add('setDatePickerDate', (selector, date) => { const monthsShort = [ 'janv.', 'févr.& ...

Advanced Layout: Angular Event window:scroll not Triggering

One issue I am facing is that the event gets triggered on some components but not others. For example, it fires as soon as I route to all other components except for the Landing component. Below are my code snippets: <-- Main Component --> <div c ...

Deep linking in Angular fails to refresh when the application is being hosted

I am currently developing a project using Angular 7 and I'm running into an issue with my deep link (www.example.com/deeplink) when the app is hosted. Everything functions perfectly during development, but after hosting, if I refresh the page it becom ...

How can I convert select all to unselect all in ngmultiselect within the Angular framework?

My attempt at resolving the issue is as follows, but it seems to be malfunctioning: (<HTMLInputElement>(<HTMLInputElement>document.getElementById("Categorydropdown")).children[0].children[1].children[0].children[0].children[0]).check ...

How can we effectively test arrow functions in unit tests for Angular development?

this.function = () => { -- code statements go here -- } I am looking to write jasmine unit tests in Angular for the function above. Any suggestions on how to achieve this? it("should call service",()=>{ // I want to invoke the arrow funct ...

Using [file_id] as a dynamic parameter in nextjs pages

I am working with a nextjs-ts code in the pages/[file_id].tsx file. import Head from 'next/head'; import Script from 'next/script'; import Image from 'next/image'; import Link from 'next/link'; import { NextApiReques ...

Issue accessing page from side menu in Ionic 2 application

I am experiencing an issue where the page does not open when I click on it in the side menu. Here is my app.component.ts file: this.pages = [ { title: 'NFC Page', component: NfcPage, note: 'NFC Page' }, ...

Make sure that every component in create-react-app includes an import for react so that it can be properly

Currently, I am working on a TypeScript project based on create-react-app which serves as the foundation for a React component that I plan to release as a standalone package. However, when using this package externally, I need to ensure that import React ...

TypeScript: custom signatures for events in a subclass of EventEmitter

Within my programming project, I have a foundational class called EventEmitter, equipped with the on method for attaching handlers to specific events: class EventEmitter { on(event: string, handler: Function) { /* internally add new handler */ ...

Struggling to implement a singleton service in Angular as per the documentation provided

I have implemented a service in Angular that I want to be a singleton. Following the guidelines provided in the official documentation, I have set the providedIn property to "root" as shown below: @Injectable({ providedIn: "root" }) export class SecuritySe ...