我有一个函数fn
,它有两个可能的调用签名:
enum Mode {
NEEDS_ARG,
ANYTHING_ELSE
}
fn(Mode.NEEDS_ARG, arg) // Call signature 1
fn(Mode.ANYTHING_ELSE) // Call signature 2
fn(Mode.ANYTHING_ELSE, arg) // Invalid
根据第一个参数的值,我想禁止第二个参数。
这是我尝试输入这样一个函数:
enum Mode {
NEEDS_ARG,
ANYTHING_ELSE
}
type Arg = "argument"
function fn(
mode: Mode,
arg: typeof mode extends Mode.NEEDS_ARG ? Arg : never
) {
arg // TS infers this as 'never'
}
来自Typescript docs关于条件类型:
When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).
我明白为什么我的实现不起作用 - typeof mode
计算为 Mode
,它不可分配给 Mode.NEEDS_ARG
。
但是输入这个函数的正确方法是什么?
编辑
我在尝试采用@jcalz提供的通用参数方法时遇到了几个问题
问题 1:arg
类型不会根据 mode
的值缩小
我预计当 mode === Mode.NEEDS_ARG
时 arg
的类型会缩小,但事实并非如此
function fn<T extends Mode>(
mode: T,
...rest: T extends Mode.NEEDS_ARG ? [arg: Arg] : []
) {
const [arg] = rest
if(arg !== undefined) {
arg // Type is narrowed to 'Arg'
}
if(mode === Mode.NEEDS_ARG) {
arg // Typescript still thinks this is 'Arg | undefined'
}
}
问题 2:没有扩展语法就无法工作
如果我用单个 arg
参数替换 ...rest
参数,Typescript 会提示
function fn<T extends Mode>(
mode: T,
arg: T extends Mode.NEEDS_ARG ? Arg: never
) {}
fn(Mode.NEEDS_ARG, arg); // Ok
fn(Mode.NO_ARG); // Err: 'An argument for 'arg' was not provided.'
最佳答案
所以你想允许这些调用:
fn(Mode.NEEDS_ARG, arg); // okay
fn(Mode.ANYTHING_ELSE); // okay
虽然不允许这些:
fn(Mode.NEEDS_ARG); // error!
fn(Mode.ANYTHING_ELSE, arg); // error!
在 TypeScript 中,有多种方法可以实现此目的。
传统的解决方案是使用两个调用签名将 fn()
变成 overloaded function:
// call signatures
function fn(mode: Mode.NEEDS_ARG, arg: Arg): void;
function fn(mode: Mode.ANYTHING_ELSE): void;
// impl
function fn(mode: Mode, arg?: Arg) {
}
现在,如果您有具有相同返回类型的重载,则可以将重载重构为 rest parameter,其类型为 union 的 tuples :
function fn(
...args: [mode: Mode.NEEDS_ARG, arg: Arg] | [mode: Mode.ANYTHING_ELSE]
): void {
}
或者,您可以将函数设置为 generic 并使用 conditional type 根据第一个参数的特定类型允许/禁止第二个参数:
function fn<T extends Mode>(
mode: T,
...rest: T extends Mode.NEEDS_ARG ? [arg: Arg] : []
): void {
}
由于函数参数的数量根据 T
变化,因此您无法将其重写为没有剩余参数的单个调用签名。已经做了一些工作让您将参数标记为类型 void
并且它是可选的,但它不适用于泛型(请参阅 microsoft/TypeScript#29131 ),因此这是不可能的。您需要一个剩余参数才能以这种方式编写调用签名。
所有这些都应该在某种程度上从调用者的角度发挥作用。这或多或少是对最初提出的问题的回答的结束。
但在实现中,存在局限性。如果您确实想将参数视为 discriminated union 并根据 mode
是否为 Mode.NEEDS_ARG
来缩小范围,并且希望编译器实现类型安全,则非常需要使用元组联合技术:
function fn(
...args: [mode: Mode.NEEDS_ARG, arg: Arg] | [mode: Mode.ANYTHING_ELSE]
): void {
const mode = args[0];
if (mode === Mode.NEEDS_ARG) {
const arg = args[1];
arg.toUpperCase(); // okay
} else {
args.length // known to be 1
}
}
在函数内部,我们从 mode
获得一个名为 args
的变量,这样当我们检查 mode
时,它充当 args
的判别式(此支持是 added in TS4.3 ;在此之前,您需要在各处都说 args[0]
而不是 0x1045 67915)。
它虽然不漂亮,但很有效。
如果您尝试使用重载进行此操作,您的实现类型将过于松散,并且将无法意识到检查 mode
对 mode
的类型有任何影响
// call signatures
function fn(mode: Mode.NEEDS_ARG, arg: Arg): void;
function fn(mode: Mode.ANYTHING_ELSE): void;
// impl
function fn(mode: Mode, arg?: Arg) {
if (mode === Mode.NEEDS_ARG) {
arg.toUpperCase(); // error
}
}
您当然可以使用像 arg
这样的类型断言,但这不是很类型安全。重载实现有点与调用签名“切断”,并且不会根据控制流“修剪”调用签名集。请参阅 microsoft/TypeScript#22609 以获取支持此功能的相关功能请求。
另一方面,如果您尝试使用通用条件来执行此操作,则会遇到类似的问题;在这种情况下,这是因为编译器不知道 arg!.toUpperCase()
在主体内是什么,因此不知道如何评估 T
:
function fn<T extends Mode>(
mode: T,
...rest: T extends Mode.NEEDS_ARG ? [arg: Arg] : []
): void {
rest // T extends Mode.NEEDS_ARG ? [arg: "argument"] : []
if (mode === Mode.NEEDS_ARG) {
rest[1].toUpperCase(); // error
}
}
同样,您可以使用类型断言或其他方法来抑制错误,但目前泛型函数的主体不知道如何使用条件类型做很多事情。您无法通过检查泛型类型参数的值来缩小其类型。请参阅 https://github.com/microsoft/TypeScript/issues/27808 以获取有助于解决此问题的功能请求。
所以就这样吧。您有多种选项可以为函数的调用签名提供类型。如果您需要在实现内部实现类型安全,最接近的方法是使用 union-of-rest-tuple 方法。否则你将不得不放弃一些类型安全。
关于typescript - 根据枚举值有条件地禁止函数参数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70764817/