Question:
TypeScript omit parameter of function based on generics

Query: Why TypeScript omit parameter of function or make it optional based on generics


How to omit or at least make optional the last parameter of function based on generics?

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 })  // that's ok, second parameter is required


Here I should be able to not pass anything as second parameter since data in patch has nothing required


makeRequest('patch', {})   

 makeRequest('patch')  // this gives an error: Expected 2 arguments, but got 1  


Here the second parameter should be omitted, since it does not have data. If not possible to omit it completely, at least the empty object should not be required


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

 

Solution

Without using function overloads, you would have to conditionally make the second parameter required. The only way to do this would be to destructure a conditionally typed tuple.


A more experienced TypeScript conjurer may find a way to reduce this to only 1 conditional, but alas, here's what I came up with:

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]) => { /* ... */ }


We still have your original check that tests if it even has a data property. However, I will note that for this check to work properly, you can't have never in Requests (never is an empty union so it breaks the distributive conditional type).


I would just keep it simple and use undefined or void in Requests:


interface Requests {

     post: {

         data: {

             test: number

         }

     }

     patch: {

         data: {

            test?: number

         }

     }

     get: undefined // changed never to undefined

}


And finally, the special condition I added, Partial<Data> extends Data, is just a way to see if everything in Data is optional (because only then would Partial<T> be assignable to T).


It is worth to note that this kind of typing would likely cause type errors when trying to create the implementation for makeRequest.

>Playground


Here's what overloads could look like. You could definitely have 3 signatures, but my implementation got too complicated for what I would like...


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


Suggested blogs:

>Why Typescript does not allow a union type in an array?

>Narrow SomeType vs SomeType[]

>Create a function that convert a dynamic Json to a formula in Typescript

>How to destroy a custom object within the use Effect hook in TypeScript?

>How to type the values of an object into a spreadsheet in TypeScript?

>Type key of record in a self-referential manner in TypeScript?

>How to get the last cell with data for a given column in TypeScript?

>Ignore requests by interceptors based on request content in TypeScript?

>Create data with Typescript Sequelize model without passing in an id?

>How to delete duplicate names from Array in Typescript?


Nisha Patel

Nisha Patel

Submit
0 Answers