推理泛型类型有什么问题
What is wrong with inference generic types
我写了一个函数包装器 (callApi
),它会自动将一些字段添加到传递给包装函数 (service
) 的参数中。并尝试推断包装函数中传递的参数类型的简短版本,以便在调用包装器时使用此类型(请参阅最后一个表达式 callApi
)。
当用户传递一些不同的参数时,键入应该突出显示错误,并且应该允许从包装函数传递所有参数(示例中的service
)
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 类型系统的结构性质,类型非常广泛*。与通常的直觉相反,它并不意味着“空对象”,而是不受约束的对象类型。因此,当一个人传递额外的属性时,编译器可以接受。
现在,通过检查 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
});
* 请注意,空对象字面量(当您将空对象分配给变量时,例如 const obj = {};
)实际上意味着空对象,因为对象字面量中有过多的 属性 检查。
我写了一个函数包装器 (callApi
),它会自动将一些字段添加到传递给包装函数 (service
) 的参数中。并尝试推断包装函数中传递的参数类型的简短版本,以便在调用包装器时使用此类型(请参阅最后一个表达式 callApi
)。
当用户传递一些不同的参数时,键入应该突出显示错误,并且应该允许从包装函数传递所有参数(示例中的service
)
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 不会完全评估 OmitcallApi
被实际使用,并且泛型 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 类型系统的结构性质,类型非常广泛*。与通常的直觉相反,它并不意味着“空对象”,而是不受约束的对象类型。因此,当一个人传递额外的属性时,编译器可以接受。
现在,通过检查 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
});
* 请注意,空对象字面量(当您将空对象分配给变量时,例如 const obj = {};
)实际上意味着空对象,因为对象字面量中有过多的 属性 检查。