Infer one of generic types from function argument

后端 未结 2 437
挽巷
挽巷 2020-12-20 04:33

Consider the following example. fetchItems function returns response or response body depending on passed onlyBody argument which defaults to

相关标签:
2条回答
  • 2020-12-20 04:56

    Function overloads can do what you need:

    function fetchItems<T>(url: string, onlyBody: false): Promise<HttpResponse<T>>
    function fetchItems<T>(url: string, onlyBody?: true): Promise<T>
    function fetchItems<T>(url: string, onlyBody: boolean = true) {
      return Promise
        .resolve({body: 'some data'} as any)
        .then(res => onlyBody ? res.body : res);
    }
    

    Playground

    Solution with conditional types does not work due to TypeScript "design limitation" described here.

    0 讨论(0)
  • 2020-12-20 05:10

    The main problem you're facing is that TypeScript does not support partial type parameter inference. Either you must manually specify all type parameters (except for ones with defaults), or you let the compiler infer all type parameters, but you cannot specify some and let the compiler infer the rest.

    Using overloads instead of generic type parameters, as shown in @Nenad's answer is one way around this for types like boolean with a small number of possible values. The issue mentioned in the comments with a boolean parameter (instead of a true or false one) can be solved by adding another overload, like this:

    function fetchItems<T>(
      url: string,
      onlyBody: false
    ): Promise<HttpResponse<T>>;
    function fetchItems<T>(url: string, onlyBody?: true): Promise<T>;
    
    // add this overload
    function fetchItems<T>(
      url: string,
      onlyBody: boolean
    ): Promise<T | HttpResponse<T>>;
    
    function fetchItems<T>(url: string, onlyBody: boolean = true) {
      return Promise.resolve({ body: "some data" } as any).then(
        res => (onlyBody ? res.body : res)
      );
    }
    
    const a = fetchItems<string>("url", false); // Promise<HttpResponse<string>>
    const b = fetchItems<string>("url", true); // Promise<string>
    const c = fetchItems<string>("url"); // Promise<string>
    const d = fetchItems<string>("url", Math.random() < 0.5); 
    // Promise<string|HttpResponse<string>>
    

    I know of two other workarounds, which I've been calling Currying and Dummying:


    The "Currying" workaround splits your single generic function of two type parameters into two curried functions of one type parameter each. One you specify, the other you infer. Like this:

    const fetchItems = <T>() => <B extends boolean = true>(
      url: string,
      onlyBody: B = true as B
    ) => {
      return Promise.resolve({ body: "some data" } as any).then<
        B extends true ? T : HttpResponse<T>
      >(res => (onlyBody ? res.body : res));
    };
    

    And you call it like this:

    const a = fetchItems<string>()("url", false); // Promise<HttpResponse<string>>
    const b = fetchItems<string>()("url", true); // Promise<string>
    const c = fetchItems<string>()("url"); // Promise<string>
    const d = fetchItems<string>()("url", Math.random() < 0.5); 
    // Promise<string|HttpResponse<string>>
    

    Or, since all of those use fetchItems<string>(), you can save that to its own function and use it, for a bit less redundancy:

    const fetchItemsString = fetchItems<string>();
    const e = fetchItemsString("url", false); // Promise<HttpResponse<string>>
    const f = fetchItemsString("url", true); // Promise<string>
    const g = fetchItemsString("url"); // Promise<string>
    const h = fetchItemsString("url", Math.random() < 0.5); 
    // Promise<string|HttpResponse<string>>
    

    The "Dummying" workaround lets the compiler infer all the parameter types, even the ones you want to specify manually. It does this by having the function take dummy parameters of the types you would normally specify manually; the function ignores the dummy parameters:

    function fetchItems<T, B extends boolean = true>(
      dummyT: T,
      url: string,
      onlyBody: B = true as B
    ) {
      return Promise.resolve({ body: "some data" } as any).then<
        B extends true ? T : HttpResponse<T>
      >(res => (onlyBody ? res.body : res));
    }
    
    const a = fetchItems("dummy", "url", false); // Promise<HttpResponse<string>>
    const b = fetchItems("dummy", "url", true); // Promise<string>
    const c = fetchItems("dummy", "url"); // Promise<string>
    const d = fetchItems("dummy", "url", Math.random() < 0.5); 
    // Promise<string|HttpResponse<string>>
    

    Since the dummy value is only for the benefit of the compiler and is unused at runtime, you can also use a type assertion to pretend you have an instance of the type instead of going through any trouble to create one:

    const dummy = null! as string; // null at runtime, string at compile time
    
    const e = fetchItems(dummy, "url", false); // Promise<HttpResponse<string>>
    const f = fetchItems(dummy, "url", true); // Promise<string>
    const g = fetchItems(dummy, "url"); // Promise<string>
    const h = fetchItems(dummy, "url", Math.random() < 0.5); 
    // Promise<string|HttpResponse<string>>
    

    Of course it's pretty easy to get a string value, so there's not much point using null! as string instead of "randomString", but for more complicated types it becomes more convenient to use the type assertion instead of trying to create a real instance that you'll just be throwing away.


    Anyway, hope one of those works for you. Good luck!

    Link to code

    0 讨论(0)
提交回复
热议问题