推理泛型类型有什么问题

时间:2021-03-18 20:32:20

标签: typescript generics type-inference typescript-generics

我编写了一个函数包装器 (callApi),它会自动向传递给包装函数 (service) 的参数添加一些字段。并尝试推断包装函数中传递的参数类型的简短版本,以便在调用包装器时使用此类型(请参阅最后一个表达式 callApi)。

当用户传递一些不同的参数时,输入应该突出显示错误,并且应该允许传递来自包装函数的所有参数(示例中的 service

Here is playground

type RequiredParams = {
    requiredParam?: string;
};

type ApiFunc<Params, Returned> = (
    params?: Omit<Params, 'requiredParam'>,
) => Promise<Returned>;

export type CallApiFunc = <Returned, Params extends RequiredParams>(
    func: ApiFunc<Params, Returned>,
    params?: Omit<Params, 'requiredParam'>,
) => Promise<Returned>;

const callApi: CallApiFunc = (
    func, 
    params, 
) => {
    return func({
      ...params,
      requiredParam: 'somevalue-maybe-auth-token'
    });
};

const service = function ({arg} : {arg: number, requiredParam: string}) {
  return null;
}

callApi(service, {
  arg: 3, // Should not be error
  // test: 'ohoh', // Should be error
});

3 个答案:

答案 0 :(得分:2)

编译器不接受 service 作为 callApi 的输入的原因是这一行 func: ApiFunc<Params, Returned>。您希望 service 函数是具有完整参数的原始函数,而不是在省略特定参数的情况下有所不同。您只想省略 callApi 的 params-arg 参数。我会像 this 一样为您的类型建模:

type RequiredParams = {
    requiredParam: string;
};

type OmitNonEmpty<Params, Keys extends string | number | symbol> = keyof Omit<Params, Keys> extends never
    ? never
    : Omit<{ requiredParam: string }, Keys>;

function callApi<Returned, Params extends RequiredParams>(
    func: (args: Params) => Returned,
    params?: OmitNonEmpty<Params, 'requiredParam'>,
): Returned {
    const requiredParams = {
        requiredParam: 'somevalue-maybe-auth-token',
        ...params,
    } as Params;

    return func(requiredParams);
}

const service = function (args: { arg: number; requiredParam: string }) {
    return Promise.resolve(null);
};

const service2 = function (args: { requiredParam: string }) {
    return Promise.resolve(null);
};

const businessCase = callApi(service, {
    arg: 3, // Should not be error
    // test: 'ohoh', // Should be error
});

const businessCase2 = callApi(service2, {
    arg: 3,
}); // doesn't work

const businessCase3 = callApi(service2); // works


这部分遇到了打字稿的限制:

const requiredParams = {
    requiredParam: "somevalue-maybe-auth-token",
    ...params
} as Parameters<typeof func>[number];

Typescript 不会完全评估 Omit 的类型,直到 callApi 被实际使用,并且泛型 Param 被替换为真实类型。但是由于 const requiredParams 的定义是在使用 callApi 之前进行的,因此打字稿不会识别 RequiredParams & Omit<Params, 'requiredParam'> 等于 Params extends RequiredParams。这就是为什么我们必须在这里进行类型转换。

答案 1 :(得分:2)

问题

callApi 充当某个函数 func 的包装器。我们用一组不完整的 callApi 调用 params,并且在调用 {requiredParam: string} 之前用 func 扩充这些参数。

您现在遇到的错误:

<块引用>

'{ requiredParam: "somevalue-maybe-auth-token" 类型的参数; }' 不可分配给类型为 'Omit' 的参数。`

是由于 ApiFunc 的定义错误造成的。这是我们在添加 requiredParam 之后调用的函数,因此它的参数类型应该是 Params 而不是 Omit<Params, 'requiredParam'>

修复之后,我们得到一个不同的错误:

<块引用>

'{ requiredParam: string; 类型的参数}' 不能分配给类型为 'Params' 的参数。

'{ requiredParam: 字符串; }' 可分配给“Params”类型的约束,但“Params”可以使用约束“RequiredParams”的不同子类型进行实例化。

这是因为您已将 params 设为可选,即使泛型类型参数 undefined 具有必需的属性,它也可以是 Params。因此,我们无法确保将所有必要的参数传递给 func

但是我们也没有对您使用 callApi(service 的示例进行良好的类型推断。所以我会稍微改变一下类型并使用 func 作为泛型。

解决方案

这实际上就是您所需要的:

const callApi = <F extends (params: any) => any> (
    func: F,
    params: Omit<Parameters<F>[0], 'requiredParam'>, 
): ReturnType<F> => {
    return func({
      ...params,
      requiredParam: 'somevalue-maybe-auth-token'
    });
};
  • 第一个参数是一个函数 F,它有一个参数:F extends (params: any) => any
  • 第二个参数是 F 的参数,没有我们添加的参数:Omit<Parameters<F>[0], 'requiredParam'>
  • 返回类型与F的返回类型相同:ReturnType<F>

如果您在 callApi 上使用 service 时不带参数或带额外参数,则会出现错误,但您可以使用正确的 arg 调用它。

Typescript Playground Link

答案 2 :(得分:1)

作为对 Linda's 的补充回答,解决了 {} 类型与具有任意数量的任意类型属性的对象兼容的问题:


由于 TypeScript 类型系统的结构特性,

{} 类型非常广泛*。与正常的直觉相反,它并不意味着“空对象”,而是一种不受约束的对象类型。因此,当传递额外的属性时,编译器可以接受。

现在,通过检查无约束对象类型是否可分配给我们的约束类型(Omit<Parameters<F>[0]),可以轻松解决这个问题。这只会发生在它们是一且相同的情况下,因此,检查器的真正分支应该是 never:

{} extends Omit<Parameters<F>[0], 'requiredParam'> ? never : Omit<Parameters<F>[0], 'requiredParam'>

让我们测试更新后的类型:

const callApi = <F extends (params: any) => any> (
    func: F,
    params: {} extends Omit<Parameters<F>[0], 'requiredParam'> ? never : Omit<Parameters<F>[0], 'requiredParam'>, 
): ReturnType<F> => {
    return func({
      ...params,
      requiredParam: 'somevalue-maybe-auth-token'
    });
};

const service2 = function ({ arg } : { arg: number, requiredParam: string }) {
  return Promise.resolve(arg);
}

callApi(service2, {}); //Property 'arg' is missing in type '{}'

callApi(service2, {
  arg: 3, // OK
  test: 'ohoh', // Object literal may only specify known properties, and 'test' does not exist
});

callApi(service2, {
  arg: 3, //OK
});

Playground


* 请注意,空对象字面量(当您将空对象分配给变量时,例如 const obj = {};)确实意味着由于对象字面量中的过多属性检查而导致的空对象。