Search code examples
typescripttypescript-typingstypescript-generics

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