我编写了一个函数包装器 (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
});
答案 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 不会完全评估 OmitcallApi
被实际使用,并且泛型 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
调用它。
答案 2 :(得分:1)
作为对 Linda's 的补充回答,解决了 {}
类型与具有任意数量的任意类型属性的对象兼容的问题:
{}
类型非常广泛*。与正常的直觉相反,它并不意味着“空对象”,而是一种不受约束的对象类型。因此,当传递额外的属性时,编译器可以接受。
现在,通过检查无约束对象类型是否可分配给我们的约束类型(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 = {};
)确实意味着由于对象字面量中的过多属性检查而导致的空对象。