【问题标题】:Infer one of generic types from function argument从函数参数推断泛型类型之一
【发布时间】:2019-08-21 10:00:40
【问题描述】:

考虑以下示例。 fetchItems 函数根据传递的 onlyBody 参数返回响应或响应正文,默认为 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);
}

如果两个泛型类型都被传递,则函数按预期工作

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>

我想放弃传递B 类型的要求,因为它相对于onlyBody 参数是多余的。但是当 B 类型没有明确传递时,ts 编译器会抱怨它(预期 2 个类型参数但得到 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>

我尝试将功能签名更改为:

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

但是在e 示例中出现错误:Argument of type 'false' is not assignable to parameter of type 'true | undefined'

有没有办法改变函数签名,使 e、f、g 示例与 a、b、c 一样工作? 演示:https://stackblitz.com/edit/typescript-ydkmzk

【问题讨论】:

    标签: typescript


    【解决方案1】:

    您面临的主要问题是 TypeScript 不支持 partial type parameter inference。要么您必须手动指定所有类型参数(具有默认值的参数除外),要么让编译器推断所有类型参数,但您不能指定一些并让编译器推断其余部分。

    使用重载而不是泛​​型类型参数,如@Nenad's answer 所示,对于像boolean 这样具有少量可能值的类型是一种解决方法。 cmets 中提到的带有boolean 参数(而不是truefalse 一个)的问题可以通过添加另一个重载来解决,如下所示:

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

    我知道另外两种解决方法,我一直称之为 Currying 和 Dummying:


    “Currying”解决方法将两个类型参数的单个泛型函数拆分为两个 curried functions,每个都具有一个类型参数。一个你指定,另一个你推断。像这样:

    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));
    };
    

    你这样称呼它:

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

    或者,由于所有这些都使用fetchItems&lt;string&gt;(),您可以将其保存到自己的函数中并使用它,以减少冗余:

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

    “Dummying”解决方法让编译器可以推断所有参数类型,甚至是您想要手动指定的参数类型。它通过让函数采用您通常手动指定的类型的虚拟参数来做到这一点;该函数忽略了虚拟参数:

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

    由于虚拟值只是为了编译器的利益而在运行时未使用,因此您也可以使用 type assertion 来假装您拥有该类型的实例,而不是费力地创建一个实例:

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

    当然,获得string 值很容易,因此使用null! as string 代替"randomString" 没有多大意义,但是对于更复杂的类型,使用类型断言而不是尝试创建更方便一个真实的例子,你会扔掉的。


    无论如何,希望其中一个对你有用。祝你好运!

    Link to code

    【讨论】:

    • 谢谢,添加您建议的超载解决了问题。顺便说一句,很好的解决方法,从来没有想过使用泛型的柯里化,肯定会尝试一下。
    【解决方案2】:

    函数重载可以满足你的需要:

    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

    由于here 描述的 TypeScript“设计限制”,条件类型的解决方案不起作用。

    【讨论】:

    • 这个解决方案的问题是它不允许将布尔参数作为onlyBody 传递。例如这种用法:function fetchUsers(onlyBody: boolean) { return fetchItems&lt;string&gt;('/users', onlyBody); } 将导致错误 Argument of type 'boolean' is not assignable to parameter of type 'true | undefined'. 也可以通过对此函数进行重载来解决,但是如果有很多这样的函数,这样的解决方案会很混乱。
    • 是的,重载似乎是唯一的方法。但是您只能将它们放在顶级方法上。您不需要在所有事情上都使用它们。
    猜你喜欢
    • 1970-01-01
    • 2020-03-12
    • 1970-01-01
    • 2016-11-01
    • 1970-01-01
    • 2020-03-22
    • 2022-01-18
    • 2020-03-11
    • 2016-12-05
    相关资源
    最近更新 更多