Understanding TypeScript's ability to infer types in generics

Exploring the world of TypeScript through a robustly typed system for REST requests.

Let's dive into the code:

This type is used to establish the connection between routes and their respective object types:

export interface RoutesMapping {
    api1: {
        users: UserApiModel
        products: ProductApiModel,
    }
    api2: {
        "other-route": OtherModel1,
        "another-one-route": OtherModel2
    }
}

export type ApiScope = keyof RoutesMapping

Here's the function that will handle POST requests:

export type RestApiPostResponse<T = any> = {
    result: boolean
    data: T
}

export function restPost<S extends keyof RoutesMapping = "api1", T extends keyof RoutesMapping[S] = keyof RoutesMapping[S]>(
  route: T,
  // this object is irrelevant to our focus 
  options: ApiRequestOptions<S, any> = {}
): Promise<RestApiPostResponse<RoutesMapping[S][T]>> {
  const url = apiUtils.buildUrl(route as string, options)
  const { payload, headers, isProtected } = options
  return post({
    url,
    isProtected,
    payload,
    headers
  });
}

To call the function, use it like this:

const data = await restPost("users")

And let TypeScript determine the return type based on scope and route.

While using the default type parameters works well:

https://i.stack.imgur.com/nQ2A7.png

The issue arises when trying to do so with another API:

const data = await restPost<"api2">("other-route")

Unfortunately, all possible types are being inferred instead of the expected one.

https://i.stack.imgur.com/G2fqZ.png

To resolve this, explicitly specify the second type parameter:

https://i.stack.imgur.com/SKI3A.png

Is there a way to achieve this without manually providing the second type parameter?

Explore more in the TypeScript playground

Answer №1

By deducing the api-key type parameter, you have the ability to create a solution that fulfills your requirements:

type Model<Route> = // Retrieves the model value for key Route in RoutesMapping
  keyof RoutesMapping extends infer Api
  ? Api extends keyof RoutesMapping 
    ? Route extends keyof RoutesMapping[Api]
      ? RoutesMapping[Api][Route]
      : never
    : never
  : never

type Routes<Api> = Api extends {} ? keyof Api  : never // A utility to spread keyof over a union of objects
type AllRoutes = Routes<RoutesMapping[keyof RoutesMapping]> // Union of all route keys: 'users' | 'products' | 'other-route' | 'another-one-route'

export function restPost<Route extends AllRoutes>(
  route: Route,
  options?:{url:string,payload:any}
): Promise<RestApiPostResponse<Model<Route>>> {
 ..
}

When utilizing a route string, the correct return type for restPost is automatically inferred, without requiring any explicit type parameters:

const data = await restPost("users") // data: RestApiPostResponse<UserApiModel>
const data2 = await restPost("other-route") // data2: RestApiPostResponse<OtherModel1>

TypeScript playground

It should be noted that this assumes unique route keys, which seems to be the case as no api key is provided to restPost. The introduction of complexity may not be advisable, but it is feasible.

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

Implementing TypeScript/Angular client generation using Swagger/OpenAPI in the build pipeline

Generating Java/Spring Server-Stubs with swagger-codegen-maven-plugin In my Spring Boot Java project, I utilize the swagger-codegen-maven-plugin to automatically generate the server stubs for Spring MVC controller interfaces from my Swagger 2.0 api.yml fi ...

Identifying an Incorrect Function Call in a TypeScript Function from a JavaScript File [TypeScript, Vue.js, JavaScript]

I have a vue2 application and I am looking to incorporate TypeScript into some service files without modifying the existing js/vue files. To enable TypeScript support, I utilized vue-cli which allowed me to successfully add a myService.ts file containing ...

Learn how to connect a formArray from the parent component to the child component in Angular with reactive forms, allowing you to easily modify the values within the formArray

In my parent component, there is a reactive form with controls and a form group. When the user selects a playerType from a dropdown menu, I dynamically add a formArray to the formGroup. This form array will contain either 2 or 3 form groups based on the p ...

I am disappointed with the lack of functionality in Angular's HTML type inference

When working inside an Angular component, I want to select a div element by id or class. This method works menuDisplayDiv = document.getElementsByClassName("some_class")[0] as HTMLDivElement; menuDisplayDiv = document.getElementById("some ...

Guide to building a nested React component

My custom dropdown component requires 2 props: trigger (to activate the dropdown) list (content to display in the dropdown) Below is the implementation of my component: import { useLayer } from "react-laag"; import { ReactElement, useState } fr ...

What is the reason for the manual update of a view when copying an object's attributes into another object, as opposed to using Object.assign()?

In my application, I have a parent component called 'EmployeeComponent' that is responsible for displaying a list of employees. Additionally, there is a child component named 'EmployeeDetailComponent' which displays the details of the s ...

Encountering the "Maximum Update Depth Exceeded" error in React Typescript with hooks

I encountered this error: Uncaught Error: Maximum update depth exceeded. It seems to be related to calling setState multiple times within componentWillUpdate or componentDidUpdate. React limits nested updates to prevent infinite loops. I am unsure of what ...

I'm looking for the configuration of function definitions for the Jasmine npm module within an Angular project. Can

When a new Angular project is created, the *.spec.ts files provide access to Jasmine functions such as "describe", "beforeEach", and expect. Despite not having an import clause for them in spec.ts files, I can click on these functions and navigate to their ...

Where can I locate htmlWebpackPlugin.options.title in a Vue CLI 3 project or how can I configure it?

After creating my webpage using vue cli 3, I decided to add a title. Upon examining the public/index.html file, I discovered the code snippet <title><%= htmlWebpackPlugin.options.title %></title>. Can you guide me on how to change and cu ...

Ways to initiate update notification when altering an array object?

I am working on my Angular4 app and I have a component that uses a *ngFor directive to iterate over an array: <div *ngFor="let person of persons"> {{person.name}} {{person.car}} </div> Within the same component, there is a feature to ...

Is there a way to automatically scroll to the bottom of a div when it first

Looking to enhance my application with a chat feature that automatically scrolls to the bottom of the chat page to display the latest messages. Utilizing VueJs: <template> <div id="app"> <div class="comments" ...

Universal Parameter Typing in Functions

I'm grappling with a concept that seems obvious to me, yet is disallowed by Typescript when all strict flags are enabled (presumably for valid reasons). Let me illustrate: We all understand the following: export interface Basic { value: "foo&q ...

Issue: (SystemJS) the exports variable is not defined

In the process of developing a .net core mvc + angular application, I encountered an interesting situation. The MVC framework handles user management, and Angular takes over when users navigate to specific areas of the application. Initially, I integrated ...

Merge two arrays by matching their corresponding identifiers

I have 2 separate arrays that I need to merge. The first array looks like this: const Dogs[] = [ { id: '1', name: 'Buddy' }, { id: '2', name: 'Max' }, ] The second one: const dogAges[] = [ { id: '4&ap ...

Updating meta tags dynamically in Angular Universal with content changes

Hello, I'm encountering an issue with a dynamic blog page. I am trying to update meta tags using data fetched from the page. Here's the code snippet: getBlogPost() { this.http.get(...) .subscribe(result => { this.blogPost = re ...

React TSX file not recognizing JSON data stored in an HTML data attribute

I am having some trouble with implementing the password toggle component from the Preline UI. Here is how the component looks: "use client" import React, { ChangeEvent, MouseEventHandler, useEffect } from "react"; export default functi ...

The ESLint tool seems to be struggling to detect the package named "@typescript-eslint/eslint-plugin"

Struggling with getting ESLint to function properly on a new Angular project in VS Code. The error message I keep encountering is about failing to load "@typescript-eslint/eslint-plugin". After spending the past 3 hours troubleshooting, I have searched hig ...

Tips for continuously running a loop function until retrieving a value from an API within a cypress project

Need help looping a function to retrieve the value from an API in my Cypress project. The goal is to call the API multiple times until we receive the desired value. let otpValue = ''; const loopFunc = () => { cy.request({ method: &ap ...

The error message "Unable to access 'useContext' property of null" appeared

I am currently in the process of developing a component library using Material UI, TypeScript, and Rollup. The package has been successfully published, but I am encountering an error when trying to import it into a new project: "Uncaught TypeError: C ...