How to leverage generics in TypeScript to either omit a parameter in a function or make it optional

How can I modify a function based on generics to omit or make optional the last parameter?

interface Requests {
     post: {
         data: {
             test: number
         }
     }
     patch: {
         data: {
            test?: number
         }
     }
     get: never
 }

const makeRequest = <Method extends keyof Requests>
   (method: Method, data: Requests[Method] extends {data: infer Data} ? Data : never)
 => { /* ... */ }

makeRequest('post', { test: 1 })  // this is correct as the second parameter is required

In the case of 'patch' method where the 'data' object has no required fields, it should be possible to not pass anything as the second parameter.

 makeRequest('patch', {})   
 makeRequest('patch')  // an error is thrown: Expected 2 arguments, but received 1  

For methods like 'get' where the second parameter is irrelevant since there is no data structure defined, it should either be completely omitted or at least not require an empty object.

makeRequest('get') // error: Expected 2 arguments, but got 1  

Answer №1

When working with TypeScript without using function overloads, it becomes necessary to conditionally require the second parameter. This can be achieved by destructuring a conditionally typed tuple.

An experienced TypeScript practitioner might find a way to reduce this to just 1 conditional statement. Here is an example of how it can be done:

const makeRequest = <Method extends keyof Requests>
   (...[method, data]:
    Requests[Method] extends { data: infer Data }
      ? Partial<Data> extends Data ? [method: Method, data?: Data] : [method: Method, data: Data]
      : [method: Method]) => { /* ... */ }

The essence remains in checking for the existence of the data property. However, it is important to avoid having never in the Requests, as it would disrupt the distributive conditional type.

To simplify things and prevent potential errors, it is recommended to use undefined or void in place of never in Requests:

interface Requests {
     post: {
         data: {
             test: number
         }
     }
     patch: {
         data: {
            test?: number
         }
     }
     get: undefined // replaced never with undefined
}

Additionally, the special condition I introduced, Partial<Data> extends Data, helps determine if everything within Data is optional (which allows Partial<T> to be assignable to T).

It's important to note that while this typing approach may work well, it could potentially lead to type errors when implementing makeRequest.

Playground


In exploring what overloads could potentially look like, it's possible to have 3 signatures. However, my attempt at implementation became too complex:

type KeysThatAreObjects<T> = {
    [K in keyof T]: T[K] extends object ? K : never;
}[keyof T];

function makeRequest<
    K extends Exclude<keyof Requests, KeysThatAreObjects<Requests>>,
>(method: K): void;
function makeRequest<K extends KeysThatAreObjects<Requests>>(
    ...args: Partial<Requests[K]["data"]> extends Requests[K]["data"]
        ? [method: K, data?: Requests[K]["data"]]
        : [method: K, data: Requests[K]["data"]]
): void;
function makeRequest(method: keyof Requests, data?: unknown) {
    // implementation
}

Playground

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

Exploring the implementation of TypeScript conditional props alongside the useContext hook

Currently, in the application I am developing, there is a component named InputElement. This component allows different HTML inputs such as input, textarea, select, etc to share common styles and properties when used with react-hook-form. type Variant = ...

"In the combined declaration of 'DepartmentListComponent', all individual declarations must either be exported or kept local." - error message regarding routing components in TypeScript file

I followed a video tutorial to write this code, but it's not working as expected and is throwing some errors. Is it possible that the tutorial is outdated and using an older methodology? The code seems to be hard-coded without using any services. Her ...

Synchronizing data between parent and child components using two-way binding with emit in Vue 3 using TypeScript

Within my code, there is a child component: <template> <label :class="className + ' ctrl'"> <span><LocCtrl :page="pageName" :locKey="labelLoc" /></span> <input type=&q ...

Exploring methods to successfully upload a blob to Firebase and modify it using cloud functions

For days now, I've been attempting to successfully upload a file to firestorage using firebase functions but haven't had any luck. This is the progress I've made so far: export const tester = functions.https.onRequest(async (request, respons ...

The feature of declaration merging does not function properly with the express 4.17.* request type

Looking to enhance the Request type, I decided to create a folder @types/express. Within this folder, I included a file index.d.ts with the following content. namespace Express { interface Request { user: number; } } Upon referencing req.user in V ...

Error message is not shown by React Material UI OutlinedInput

Using React and material UI to show an outlined input. I can successfully display an error by setting the error prop to true, but I encountered a problem when trying to include a message using the helperText prop: <OutlinedInput margin="dense&quo ...

I am experiencing difficulties with my data not reaching the function in my component.ts file within Angular

My current project involves integrating Google Firebase to handle the login functionality. I encountered an issue where the data inputted from the HTML page to the component.ts file was not being processed or reaching Firebase. However, when I initialized ...

What is the significance of requiring a specific string in a Typescript Record when it is combined with a primitive type in a union?

I am facing an issue with the following data type: type ErrorMessages = Record<number | 'default', string>; When I declare a variable like const text: ErrorMessages = {403: 'forbidden'}, Typescript points out that default is miss ...

The attribute 'commentText' is not found within the 'Comment' data type

Currently, I am immersed in building a user-friendly social network application using Angular 12 for my personal educational journey. Running into an error has left me puzzled and looking for assistance. About the Application: The home page (home.compone ...

default folder location for core modules adjustment

While experimenting with module imports in TypeScript, I encountered an issue when trying to import a module using import { Component, OnInit } from '@angular/core';. The compiler was successfully finding the module in the node_modules folder. I ...

Inefficiency in POST method prevents data transmission to MongoDB

I've developed a MERN application and now I'm testing the backend using the REST client vscode extension. This is how it looks: `POST http://localhost:4000/signup Content-Type: application/json { "email": "<a href="/cdn-cgi ...

Exploring the Possibilities of Nipplejs Integration in Vue with Quasar

Trying to implement Nipplejs in my Vue Project using quasar Components. Installed nipplejs through npm install nipplejs --save. Attempted integration of the nipple with the code snippet below: <template> <div id="joystick_zone">&l ...

What is the best way to define a general class within the constructor of another class in TypeScript?

Is there a way to inject a subclass into a class during its constructor using TypeScript? I've tried to demonstrate my idea with some pseudo-code here: type GenericConstructor<T> = { new (): T; } class MyClass { constructor( SubClass: G ...

Guide on how to address the problem of the @tawk.to/tawk-messenger-react module's absence of TypeScript definitions

Is there a way to fix the issue of missing TypeScript definitions for the @tawk.to/tawk-messenger-react module? The module '@tawk.to/tawk-messenger-react' does not have a declaration file. 'c:/develop/eachblock/aquatrack/management-tool-app ...

What is the best way to retrieve a string from a URL?

Is there a way to extract only a specific string from a URL provided by an API? For instance: I'm interested in extracting only: photo_xxx-xxx-xxx.png Any suggestions on how to split the URL starting at photo and ending at png? ...

Tips on creating an object within a TypeScript interface

As a newcomer to Type Script, I am curious if there is a way to specify in the interface "IIndex" that SystemStatus is an object with properties Data and DataUrl. Currently, it appears that SystemStatus is undefined. interface IIndex extends ng.IScope { ...

The issue at hand is that the Mongo Atlas model is in place, but for some reason,

https://i.sstatic.net/4m2KT.pngI recently delved into using Next.js and I am a newcomer to backend technologies. I have successfully established a connection with MongoDB Atlas using Mongoose, however, the user object data from the post request is not be ...

Mongoose version 5.11.14 is causing TypeScript error TS2506 when attempting to utilize async iterators, requiring a '[Symbol.asyncIterator]()' to be present

I have decided to stop using @types/mongoose now that I've discovered that mongoose 5.11 already includes type information. However, when I attempt to run my code through tsc, I encounter TypeScript issues specifically with the following line: for aw ...

TypeORM ensures that sensitive information, such as passwords, is never returned from the database when retrieving a user

I developed a REST API using NestJs and TypeORM, focusing on my user entity: @Entity('User') export class User extends BaseEntity { @PrimaryGeneratedColumn() public id: number; @Column({ unique: true }) public username: string; publi ...

Identifying Typescript argument types

The given code is functional, but I am looking to refactor it for better clarity using Typescript syntax if possible. class Actions { actions: string[] } type Argument = object | Actions; export class GetFilesContext implements IExecutable { execute( ...