简体   繁体   中英

Infer one of generic types from function argument

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

interface HttpResponse<T> {
  body: T
}

function fetchItems<T, B extends boolean>(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);
}

If both generic types are passed the function works as expected

const a = fetchItems<string, false>('url', false) // Promise<HttpResponse<string>>
const b = fetchItems<string, true>('url', true)   // Promise<string>
const c = fetchItems<string, true>('url')         // Promise<string>

I'd like to drop requirement of passing B type as it is redundant in relation to onlyBody parameter. But when B type is not explictly passed, ts compiler complains about it (Expected 2 type arguments but got 1).

const e = fetchItems<string>('url', false);        // would want Promise<HttpResponse<string>>
const f = fetchItems<string>('url', true)          // would want Promise<string>
const g = fetchItems<string>('url')                // would want Promise<string>

I tried to change fucntion signature into:

function fetchItems<T, B extends boolean = true>(url: string, onlyBody: B = true as B) {

but then there is an error in e example: Argument of type 'false' is not assignable to parameter of type 'true | undefined' Argument of type 'false' is not assignable to parameter of type 'true | undefined'

Is there any way to alter function signature so that e, f, g examples will work the same as a, b, c ? demo: https://stackblitz.com/edit/typescript-ydkmzk

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 .

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 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM