typescript - 根据枚举值有条件地禁止函数参数

标签 typescript conditional-types

我有一个函数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 的值缩小

Playground

我预计当 mode === Mode.NEEDS_ARGarg 的类型会缩小,但事实并非如此

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:没有扩展语法就无法工作

Playground

如果我用单个 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,其类型为 uniontuples :

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)。

它虽然不漂亮,但很有效。


如果您尝试使用重载进行此操作,您的实现类型将过于松散,并且将无法意识到检查 modemode 的类型有任何影响

// 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 方法。否则你将不得不放弃一些类型安全。

Playground link to code

关于typescript - 根据枚举值有条件地禁止函数参数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70764817/

相关文章:

javascript - 元素隐式具有 'any' 类型,因为索引表达式不是 'number' 类型

javascript - typescript ,使用没有构造函数的类

typescript - 提取函数的返回类型并按 typescript 中的属性名称进行过滤

typescript - 具有条件类型的简单函数

javascript - 在 TypeScript 和 MongoDB 中仅获取没有时间的日期

reactjs - 键入 React 组件工厂函数

javascript - Angular 中的重复功能失败

typescript - 尝试使用 Typescript 条件类型来缩小预期值的可能组合

typescript 2.8 : Remove properties in one type from another