使用JavaScript的Flowtype解释泛型

7
我以前从未使用过静态类型语言进行编写。我主要在开发JavaScript,最近对学习更多关于FB的Flowtype感兴趣。
我发现文档写得很好,我理解了大部分内容。但是我不太明白generics的概念。我尝试谷歌一些例子/解释,但没有成功。
请问有人能解释一下什么是泛型,它们通常用于什么以及可能提供一个例子吗?
3个回答

6
假设我要编写一个只存储单个值的类。显然这是人为的; 我保持简单。实际上,这可能是一些集合,比如可以存储多个值的Array
假设我需要包装一个number:
class Wrap {
  value: number;
  constructor(v: number) {
    this.value = v;
  }
}

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

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

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

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,并在内部更改numberstring。但这很繁琐,现在我有两份相同的内容需要维护。如果每次想要包装新类型时都进行复制,这将很快失控。
但请注意,Wrap实际上并不对value进行操作。它不关心它是number还是string,或者其他什么类型。它只存在于存储和稍后返回值。这里唯一重要的不变量是您给出的值和您获取的值是相同类型。使用的具体类型无关紧要,只要这两个值具有相同的类型。
因此,在这种情况下,我们可以添加类型参数:
class Wrap<T> {
  value: T;
  constructor(v: T) {
    this.value = v;
  }
}

T在这里只是一个占位符。它的意思是,“我不关心你在这里放什么类型,但很重要的是,在使用T的任何地方,它都是相同的类型。”如果我传递给您一个Wrap<number>,您可以访问value属性,并知道它是一个number。同样地,如果我传递给您一个Wrap<string>,您就知道该实例的value是一个string。有了这个新的Wrap定义,让我们再试着包装一个number和一个string

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

(在这里查看完整示例)


谢谢,这是一个很棒的例子,但我希望它能在文档中找到!然而,我发现有一点不足的是,似乎构造类时使用的类型没有被记住。我尝试使用你的例子,如果我尝试 wNum.value = 'foo',那么静态类型检查器不会抱怨,这让我感到非常奇怪。我的期望是,由于 wNum 是用数字实例化的,它应该强制执行此限制。 - user3056783
1
这是因为如果你这样做,Flow会推断出string | number而不仅仅是number。如果你试图将其用作number,那么就会出现错误。如果你想要在前面指定类型,你可以像这样注释变量:const wNum: Wrap<number> = new Wrap(5);。然后Flow会在你尝试将wNum.value设置为string时立即给出错误提示。 - Nat Mote
@NatMote 为什么叫做 T?这只是惯用语吗?T代表什么? - Daniel Lizik
这只是惯用的写法。在我使用过的许多编程语言中,将 T 作为类型参数相当普遍。如果我要猜测,我会说它可能代表“类型”,但我不确定。 - Nat Mote

5
静态类型语言中的泛型是一种定义可应用于任何类型依赖的单个函数或类的方法,而不是为每种可能的数据类型编写单独的函数/类。它们确保一个值的类型将始终与分配给相同泛型值的另一个值的类型相同。例如,如果您想编写一个将两个参数相加的函数,在某些语言中,该操作可能完全不同。在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,而且该函数总是返回相同的类型T。在函数内部,由于我们知道参数始终为相同的类型,我们可以使用标准的JavaScript测试其中一个参数的类型,并返回我们认为该类型的“加法”结果。
在我们代码中稍后使用时,这个函数可以被调用为:
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.

谢谢,这很容易理解。我在0.96.1版本的Flow中尝试了一下,但是遇到了很多强制转换错误,尽管使用了https://flow.org/try/#0PTAEAEDMBsHsHcBQjIFcB2BjALgS1uqAIYAmJAPACoB8AFAG4CMAXKJQDSj0BMrlAlH1ABvRKFC5IoWtgCeABwCmsKU1ABedaADkAZ2wAnXOgDm2-mPGgDi7KgOEABgBJhTAL6hXPd44DclorQuooSUjIKyqqMGlrasABGAFaKOOaW4jZ2DiKgAHQFTJwFeTyg7gHiQSEZ1rb2hGoA1FzcAe7IpCS0jJzc-H5AA。 - Daniel Lizik

2
基本上,它只是类型的占位符。
当使用泛型类型时,我们在说可以在这里使用任何 Flow 类型。
通过在函数参数之前放置 <T>,我们在说该函数可以(但不一定)在其参数列表、主体和返回类型中使用一个泛型类型 T
让我们看看他们的基本示例:
function identity<T>(value: T): T {
  return value;
}

这意味着在 identity 中的参数 value 将具有某种类型,该类型事先不知道。无论那种类型是什么,identity 的返回值也必须匹配该类型。
const x: string = identity("foo"); // x === "foo"
const y: string = identity(123);   // Error

泛型的一种简单理解方式是将原始类型之一替换为T,然后了解如何工作,然后理解该原始类型可以被任何其他类型替换。
标识而言:将其视为接受[string]并返回[string]的函数。那么要理解,[string]也可以是任何其他有效的流类型。 这意味着identity是一个接受T并返回T的函数,其中T是任何流类型。
文档还提供了这个有用的类比:

泛型类型与变量或函数参数非常相似,只是它们用于类型。

注意:该概念的另一个词是多态性

1
基本上,它只是一个类型的占位符。啊,这概括得很好!谢谢。是的,我熟悉多态性这个术语,但在不同的上下文中(数据库)。 - user3056783
非常好的解释,谢谢。对我来说,这个答案是最清晰易懂的。 - Daniel Lizik
@DanielLizik 很高兴能帮忙 :) - nem035

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接