TypeScript feature: Determining return type dynamically based on object property using string literal

I am looking to enhance the functionality to dynamically determine the return type based on the string literal provided.

Current Approach:

type Baseball = {
 name: string;
 lng: number;
 lat: number;
}

type SeriesInfo = {
  series: {
    [key: string]: Baseball;
  };
};

class BaseballInfo {
    private data: SeriesInfo;
    constructor() {
        this.data = {
            'series': {
                mets:  { name: 'Mets', lng: 12.34, lat: 12.34 },
                yankees:  { name: 'Yankees', lng: 12.34, lat: 12.34 },
            }
      }
  }
  // ...
  public getName(key: string) {
    return this.getProperty(key, 'name') as Baseball['name'];
  }
  private getProperty(key: string, prop: keyof Baseball) {
    return this.data.series[key][prop];
  }
}

I want to enhance the code to dynamically adjust the return type like this:


  public getName(key: string) {
    return this.getProperty(key, 'name');
  }

  private getProperty(key: string, prop: keyof Baseball) {
    return this.data.series[key][prop] as Baseball[prop];
  }

Answer №1

Define the method getProperty with the following signature:

private getProperty<T extends keyof Baseball>(key: string, prop: T): Baseball[T] {
    return this.data.series[key][prop];
}

As a result, the function getName will now be properly typed as a string.

Answer №2

To achieve the desired behavior, you can modify getProperty() to be a generic function within K, a subtype of keyof Baseball, which will be automatically inferred during function calls. The return type of this function is automatically inferred by the compiler as Baseball[K], representing a generic lookup type denoting the K-keyed property type of a BaseBall value:

private getProperty<K extends keyof Baseball>(key: string, prop: K) {
  return this.data.series[key][prop];
}

It's worth noting that the index signature of SeriesInfo suggests that it will contain a property of type Baseball for any key utilized. However, this is improbable, and most keys will return an undefined property, resulting in seemingly correct code during design time but potential runtime errors:

new BaseballInfo().getName("red sox").toUpperCase(); // TS: okay, JS: error!

This is characteristic of index signatures (refer to microsoft/TypeScript#13778). It's uncertain how you plan to handle this situation. One approach is to have getProperty() return a value or undefined if the key is absent. In that case, adjust your types as follows:

type SeriesInfo = {
  series: {
    [key: string]: Baseball | undefined;
  };
};

To acknowledge the possibility of undefined, you can utilize the optional chaining operator (or equivalent) within getProperty():

private getProperty<K extends keyof Baseball>(key: string, prop: K) {
  return this.data.series[key]?.[prop]; // utilize the ?. operator
}

With this setup, getProperty() now returns Baseball[K] | undefined, and getName() returns

string | undefined</code, resulting in this code now being flagged as an error:</p>

<pre><code>new BaseballInfo().getName("red sox").toUpperCase(); // error!
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ <-- potentially undefined

Alternatively, you can specify the type of series by allowing the compiler to infer the keys and restricting only those keys to be input, by adding an additional generic parameter:

type Series = BaseballInfo['data']['series'];

class BaseballInfo {
  private data = {
    'series': {
      mets: { name: 'Mets', lng: 12.34, lat: 12.34 },
      yankees: { name: 'Yankees', lng: 12.34, lat: 12.34 },
    }
  };
  constructor() { }
  public getName<S extends keyof Series>(key: S) {
    return this.getProperty(key, 'name');
  }
  private getProperty<S extends keyof Series, B extends keyof Baseball>(key: S, prop: B) {
    return this.data.series[key][prop];
  }
}

As a result, the error message changes to:

new BaseballInfo().getName("red sox").toUpperCase(); // error!
// ----------------------> ~~~~~~~~~
// "red sox" is not assignable to "yankees" | "mets"

Hopefully, this resolves your issue. Best of luck!

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

Using Typescript to create mapped types with optional parameters

I am currently working on implementing a mapped type that has some required parameters and some optional ones. While I have successfully implemented the required parameters, I am facing issues with utilizing the optional ones. type Foo = { foo: string } ...

Using Angular to bind the ngModel to a variable's object property

In my application, I am working with a user object that looks like this: let user = {name: "John", dob:"1995-10-15", metadata: {}} The metadata property of the user object is initially empty. I want to add a new property to the metadata object based on u ...

What benefits does a bundler offer when releasing packages on npm?

After working with Node.js for many years, I recently ventured into publishing my first Node.JS package for a wider audience. Feeling lost at the beginning, I turned to Google for guidance on how to do this specifically for typescript and stumbled upon thi ...

What is the best way to invoke a service within a for loop?

private Payload(): asset { const payload = { //pay load } return payload; } public listofnumbers() { number = [1,2,3]; number.forEach(element => { this.service(element); }); } private service(number) { this.se ...

Encountering a console error in a TypeScript Express app when using MUI and Preact: "Unexpected prop `children` passed to `InnerThemeProvider`, was expecting a ReactNode."

I'm working on integrating MUI with a Preact app. In VSCode, everything seems to be set up correctly, but when I try to view it in the browser, nothing renders and I get this console error: react-jsx-runtime.development.js:87 Warning: Failed prop type ...

Gain access to TypeScript headers by typing the request (req) object

Is there a way to access headers in a method that is typed with Express.Request? Here's an example code snippet: private _onTokenReceived(req: Express.Request, res: Express.Response): void { const header: string = req.headers.authorizatio ...

Angular 6's subscribe method is causing the UI to not update

I'm currently facing an issue where my component does not refresh the UI after I input data. I always have to manually refresh the page to see the changes. I suspect there might be a problem with the .subscribe method in Angular 6. Previously, when I ...

Interface specifying a React.ref property

When attempting to use a string as the ref, I encountered a warning when trying to access ref?.current Property 'current' does not exist on type 'string' What should be used for the ref prop in the interface? I am uncertain of the upfr ...

Ionic 2: Issue with Custom Provider Resulting in "Unable to Resolve All Parameters"

I have created a test provider and I am attempting to inject it into two pages for the purpose of sharing data and methods. However, when I add the provider to the page constructor, an error is thrown, stating "Can't resolve all parameters for Charact ...

Ways to specify the type signature for objects that incorporate a fresh method

My understanding is that in TypeScript, we use new() to structurally type a class constructor. But how do we type an object that includes a new method, for example: const k = { new() { return '123' } } ...

Enhance the functionality of angular-material buttons by incorporating dynamic loading animations into

I am currently working on a solution in Angular 12 to disable a button (and show a spinner) when it is clicked, until the API responds. To achieve this, I plan to follow a similar approach to the angular-material button implementation. Essentially, I want ...

Connecting conversations in react

When working with jQuery, I often utilize the modal dialog chaining technique. For example: $.Deferred().resolve().promise() .then(function () { return runDialog1(someProps); // return promise }) .then(function (runDialog1Result) ...

"Learn how to utilize Angular to showcase an array of strings and choose a specific value for

When working in HTML, I have the ability to set the option text and send the value like this: <select id="cars"> <option value="volvo">Volvo</option> <option value="saab">Saab</option> </select> After sending it ...

Can you explain the significance of the | symbol in TypeScript?

My journey with TypeScript is just beginning, and I recently encountered the symbol | while working on a problem in LeetCode using Typescript. I believe it has something to do with defining variable types. Can anyone provide more insight into this? /** ...

Updating the React State is dependent on the presence of a useless state variable in addition to the necessary state variable being set

In my current setup, the state is structured as follows: const [items, setItems] = useState([] as CartItemType[]); const [id, setId] = useState<number | undefined>(); The id variable seems unnecessary in this context and serves no purpose in my appl ...

Properly incorporating a git+https dependency

I'm facing an issue while trying to utilize a git+https dependency from Github to create a TypeScript library. I've minimized it to a single file for illustration purposes, but it still doesn't work. Interestingly, using a file dependency fu ...

How can I emphasize the React Material UI TextField "Cell" within a React Material UI Table?

Currently, I am working on a project using React Material UI along with TypeScript. In one part of the application, there is a Material UI Table that includes a column of Material TextFields in each row. The goal is to highlight the entire table cell when ...

Sharing data between components in Angular 4: Passing objects between different parts of your

Exploring Angular 4 development using TypeScript: I am looking to establish a static object in app.component.ts that can be accessed in all components. Any suggestions on how to accomplish this? ...

Tips for utilizing the polymorphic feature in TypeScript?

One of the challenges I am facing involves adding data to local storage using a function: add(type: "point" | "object", body: FavouritesBodyPoint | FavouritesBodyObject) { // TODO } export interface FavouritesBodyPoint {} export in ...

The function of getTime is not available for use

I assigned the date_work property to a Date data type. However, when I check the data type using the command console.log(typeof master.date_work), it shows as a string for some reason. This causes an error when using the getTime() function. How can I conve ...