使用 Javascript 的 Flowtype 解释泛型

Explain generics using Javascript's Flowtype

我以前从未用静态类型语言编写过。我主要在 Javascript 进行开发,最近我有兴趣了解更多有关 FB 的 Flowtype 的信息。

我觉得文档写得很好,我能理解其中的大部分内容。但是我不太明白 generics 的概念。我试过用谷歌搜索一些例子/解释,但没有成功。

有人可以解释一下泛型是什么,它们主要用于什么,并提供一个例子吗?

静态类型语言中的泛型是一种定义单个函数或 class 的方法,它可以应用于任何类型依赖性,而不是为每种可能的数据类型编写单独的 function/class。他们确保一个值的类型始终与分配给相同通用值的另一个值的类型相同。

例如,如果您想编写一个将两个参数相加的函数,则该操作(取决于语言)可能完全不同。在 JavaScript 中,因为它不是一开始的静态类型语言,所以您无论如何都可以这样做并在函数内进行类型检查,但是 Facebook 的 Flow 除了单一定义之外还允许类型一致性和验证。

function add<T>(v1: T, v2: T): T {
    if (typeof v1 == 'string')
        return `${v1} ${v2}`
    else if (typeof v1 == 'object')
        return { ...v1, ...v2 }
    else
        return v1 + v2
}

在这个例子中,我们定义了一个具有通用类型 T 的函数,并说所有参数都是 相同的 类型 T 和函数将始终 return 相同 类型 T。在函数内部,因为我们知道参数总是相同的类型,所以我们可以使用标准 JavaScript 和 return 我们感知的以及 "addition" 来测试其中一个的类型那种类型。

稍后在我们的代码中使用时,可以调用此函数:

add(2, 3) // 5
add('two', 'three') // 'two three'
add({ two: 2 }, { three: 3 }) // { two: 2, three: 3 }

但是如果我们尝试会抛出输入错误:

add(2, 'three')
add({ two: 2 }, 3)
// etc.

基本上,它只是一个类型的占位符。

当使用泛型类型时,我们是说这里可以使用任何 Flow 类型。

通过将 <T> 放在函数参数之前,我们表示此函数可以(但不必)在其参数列表、函数体中的任何位置使用通用类型 T , 以及它的 return 类型。

让我们看看他们的基本例子:

function identity<T>(value: T): T {
  return value;
}

这意味着 identity 中的参数 value 将具有某种事先不知道的类型。无论哪种类型,identity 的 return 值也必须匹配该类型。

const x: string = identity("foo"); // x === "foo"
const y: string = identity(123);   // Error

思考泛型的一种简单方法是想象一种基本类型而不是 T 并看看它是如何工作的,然后理解这种基本类型可以替代任何其他类型。

identity 方面:将其视为接受 [string] 和 returns [string] 的函数。然后了解 [string] 也可以是任何其他有效的流类型。 这意味着 identity 是一个接受 T 和 return 的函数 T,其中 T 是任何流类型。

文档也有这个有用的类比:

Generic types work a lot like variables or function parameters except that they are used for types.

注意:这个概念的另一个词是polymorphism

假设我想编写一个只存储单个值的 class。显然这是人为的;我保持简单。实际上,这可能是一些集合,例如 Array,可以存储多个值。

假设我需要包装一个 number:

class Wrap {
  value: number;
  constructor(v: number) {
    this.value = v;
  }
}

现在我可以创建一个存储数字的实例,并且可以取出该数字:

const w = new Wrap(5);
console.log(w.value);

到目前为止一切顺利。但是等等,现在我也想包装一个string!如果我天真地尝试包装一个字符串,我会得到一个错误:

const w = new Wrap("foo");

给出错误:

const w = new Wrap("foo");
                       ^ string. This type is incompatible with the expected param type of
constructor(v: number) {
                    ^ number

这不起作用,因为我告诉 Flow Wrap 只需要 numbers。我可以将 Wrap 重命名为 WrapNumber,然后复制它,调用副本 WrapString,并在正文中将 number 更改为 string。但这很乏味,现在我要维护同一件事的两个副本。如果我每次想包装一个新类型时都继续复制,这很快就会失控。

但请注意 Wrap 实际上并未对 value 进行操作。它不关心它是 number 还是 string,或者其他什么。它的存在只是为了存储它并在以后归还它。这里唯一重要的不变量是你给它的值和你取回的值相同的类型。使用什么特定类型并不重要,只要这两个值具有相同的值即可。

因此,考虑到这一点,我们可以添加一个类型参数:

class Wrap<T> {
  value: T;
  constructor(v: T) {
    this.value = v;
  }
}

T 这里只是一个占位符。这意味着 "I don't care what type you put here, but it's important that everywhere T is used, it is the same type." 如果我给你一个 Wrap<number> 你可以访问 value 属性 并且知道它是一个 number。同样,如果我给你一个 Wrap<string>,你知道那个实例的 valuestring。使用 Wrap 的新定义,让我们再次尝试包装 numberstring:

function needsNumber(x: number): void {}
function needsString(x: string): void {}

const wNum = new Wrap(5);
const wStr = new Wrap("foo");

needsNumber(wNum.value);
needsString(wStr.value);

Flow 推断类型参数并且能够理解这里的一切都将在运行时工作。如果我们尝试这样做,我们也会像预期的那样得到一个错误:

needsString(wNum.value);

错误:

20: needsString(wNum.value);
                ^ number. This type is incompatible with the expected param type of
11: function needsString(x: string): void {}
                            ^ string

tryflow 完整示例)