如何在 Flow 中用多个可能的调用签名来注释一个函数?

How to annotate a function with multiple possible call signatures in Flow?

在 JavaScript 中,一个函数可以通过多种方式调用是很常见的——例如带有少量位置参数 单个选项对象 两者的某种组合。

我一直在想办法注释这个。

我尝试的一种方法是将 rest args 注释为各种可能的元组的联合:

type Arguments =
  | [string]
  | [number]
  | [string, number]
;

const foo = (...args: Arguments) => {
  let name: string;
  let age: number;

  // unpack args...
  if (args.length > 1) {
    name = args[0];
    age = args[1];
  } else if (typeof args[0] === 'string') {
    name = args[0];
    age = 0;
  } else {
    name = 'someone';
    age = args[1];
  }

  console.log(`${name} is ${age}`);
};

// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);

以上片段是人为设计的;在这个例子中我可能只使用 (...args: Array<string | number>) ,但是对于更复杂的签名(例如,涉及一个类型化的选项对象,它可以单独存在或与先前的参数一起使用),能够定义一个精确的、有限的集合将很有用可能的调用签名。

但是上面没有类型检查。您可以在 tryflow.

中看到一堆令人困惑的错误

我还尝试将函数本身键入为单独的整个函数定义的联合,但是 didn't work 要么:

type FooFunction =
  | (string) => void
  | (number) => void
  | (string, number) => void
;

const foo: FooFunction = (...args) => {
  let name: string;
  let age: number;

  // unpack args...
  if (args.length > 1) {
    name = args[0];
    age = args[1];
  } else if (typeof args[0] === 'string') {
    name = args[0];
    age = 0;
  } else {
    name = 'someone';
    age = args[1];
  }

  console.log(`${name} is ${age}`);
};

// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);

我应该如何使用多个可能的调用签名处理类型注释函数? (或者多重签名被认为是 Flow 中的反模式,我根本不应该这样做——在这种情况下,与这样做的第三方库交互的推荐方法是什么?)

在您给出的三种可能的锻炼中,我已经想出了如何使用单个选项对象使其工作,但是因为您需要至少设置一个对象,所以您需要定义每一种可能性。

像这样:

type Arguments = 
    {|
        +name?: string,
        +age?: number
    |} |
    {|
        +name: string,
        +age?: number
    |} |
    {|
        +name?: string,
        +age: number
    |};

const foo = (args: Arguments) => {
  let name: string = args.name ? args.name : 'someone';
  let age: number = typeof args.age === 'number' && !isNaN(args.age) ? args.age : 0;
  console.log(`${name} is ${age}`);
}

// any of these call signatures are OK:
foo({ name: 'fred' });
foo({ name: 'fred', age: 30 });
foo({ age: 30 });

// fails
foo({});

您看到的错误是代码中的错误和 Flow 中的错误的组合。

您的代码中存在错误

让我们从修复您的错误开始。在第三个 else 语句中,您将错误的值分配给

  } else {
    name = 'someone';
    age = args[1]; // <-- Should be index 0
  }

将数组访问更改为正确的索引可以消除两个错误。我想我们都同意这正是 Flow 的用途,即在您的代码中查找错误。

缩小型

为了找到问题的根本原因,我们可以在错误的地方更明确,这样我们就可以更容易地看到问题是什么:

if (args.length > 1) {
  const args_tuple: [string, number] = args;
  name = args_tuple[0];
  age = args_tuple[1];
} else if (typeof args[0] === 'string') {

这实际上与以前相同,但因为我们非常清楚此时 args[0]args[1] 应该是什么。这给我们留下了一个错误。

流程中的错误

剩下的错误是 Flow 中的错误:https://github.com/facebook/flow/issues/3564

bug: tuple type is not interacting with length assertions (.length >= 2 and [] | [number] | [number, number] type)

如何键入重载函数

Flow 不擅长处理不同类型的可变参数,就像在本例中一样。 Variadics 更适用于像 function sum(...args: Array<number>) 这样的东西,其中所有类型都相同并且没有最大数量。

Instead, you should be more explicit with your arguments, like so:

const foo = (name: string | number, age?: number) => {
  let real_name: string = 'someone';
  let real_age: number = 0;

  // unpack args...
  if (typeof name === 'number') {
    real_age = name;
  } else {
    real_name = name;
    real_age = age || 0;
  }

  console.log(`${real_name} is ${real_age}`);
};

// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);

这不会导致错误,而且我认为开发人员也更容易阅读。

更好的方法

在另一个回答中,比我自己更喜欢

type Foo =
  & ((string | number) => void)
  & ((string, number) => void)

const foo: Foo = (name, age) => {...};

它以更简洁的方式解决了相同的问题,让您更加灵活。通过创建多个函数类型的交集,您可以描述调用函数的每种不同方式,从而允许 Flow 根据调用函数的方式尝试每一种方式。

您可以通过 &:

连接它们来定义多个函数签名
type Foo =
  & ((string | number) => void)
  & ((string, number) => void)

Try it.