推理泛型类型有什么问题

What is wrong with inference generic types

我写了一个函数包装器 (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
});

编译器不接受 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。这就是为什么我们必须在这里进行类型转换。

问题

callApi 用作某些函数 func 的包装器。我们用一组不完整的 params 调用 callApi,并且在调用 func.

之前用 {requiredParam: string} 扩充这些参数

您现在遇到的错误:

Argument of type '{ requiredParam: "somevalue-maybe-auth-token"; }' is not assignable to parameter of type 'Omit<Params, "requiredParam">'.`

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

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

Argument of type '{ requiredParam: string; }' is not assignable to parameter of type 'Params'.

'{ requiredParam: string; }' is assignable to the constraint of type 'Params', but 'Params' could be instantiated with a different subtype of constraint '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'>
  • return类型与F的return类型相同:ReturnType<F>.

如果您在 service 上使用 callApi 时没有参数或带有额外参数,您会收到错误消息,但您可以使用正确的 arg 调用它。

Typescript Playground Link

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


{} 由于 TypeScript 类型系统的结构性质,类型非常广泛*。与通常的直觉相反,它并不意味着“空对象”,而是不受约束的对象类型。因此,当一个人传递额外的属性时,编译器可以接受。

现在,通过检查 unconstrained 对象类型是否可分配给我们的 constrained 类型(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 = {};)实际上意味着空对象,因为对象字面量中有过多的 属性 检查。