TypeScript 中的数组类型

TS的数组类型你使用哪个?Array<string>(泛型语法)与 string[](数组语法),这两种表示法在功能上完全没有区别。但其中总是涉及细微差别和权衡。通常情况下,泛型表示法要好得多。
关于这个问题时,一起分析下两种写法的不足和优势

数组语法更短

就是这样。这就是优势所在。写入的字符更少。好像代码越短就越易维护一样。
但我们更经常阅读代码而不是编写它,因此我们不应该专注于使其易于编写——它应该易于阅读。

泛型语法可读性

我们通常从左到右阅读,所以更重要的东西应该先出现。我们说:“这是一个字符串数组”,或者“这是一个字符串或数字数组”。

1
2
3
4
5
6
//从左到右
// ✅ 从左到右阅读起来很舒服
function add(items: Array<string>, newItem: string)

// ❌ 看起来与“string”非常相似
function add(items: string[], newItem: string)

如果我们数组中的类型是相当长的,则这一点尤为重要,例如因为它是从某处推断出来的。IDE 通常以数组表示法显示数组类型,所以有时,当我悬停在一个对象数组上时,我会得到这样的内容:

1
2
3
4
//options-array
const options: {
[key: string]: unknown
}[]

这读起来像是 options 是一个对象,只有在最后,我才能看到它实际上是一个数组。如果对象有很多属性,情况就会变得更糟,因为这会使内容变得更长,并且会给浮动框增加一个滚动条——使得很难看到最后的 []。诚然,这可能是一个工具问题,但如果我们将其显示为它的本质:一个对象数组,这个问题就不存在了。当跨多行展示时,它甚至不会长得多:

1
2
3
4
//array-of-options
const options: Array<{
[key: string]: unknown
}>

不管怎样,我们继续往下看,因为这不是数组表示法的唯一优点。

ReadonlyArray

让我们面对现实——我们作为函数输入的大多数数组都应该是只读的,以避免意外的突变。我也在另一篇文章中提到了这个话题。如果你使用泛型表示法,你可以简单地将Array替换为ReadonlyArray并继续前进。如果你使用数组表示法,你必须将它拆分成两部分:

1
2
3
4
5
6
7
8
9
10
11
## readonly-arrays
// ✅ 最好使用 readonly,这样你不会意外改变 items
function add(items: ReadonlyArray<string>, newItem: string)

// ❌ "readonly" 和 "Array" 现在被分开了
function add(items: readonly string[], newItem: string)
````
这并不是什么大不了的事,但 readonly 是一个只能用于数组和元组的保留字,在我们有一个内置的实用类型可以完成同样工作时,这确实很奇怪。而将 `readonly `和` [] `拆分开真的会在阅读时影响流畅性。

## Union types
如果我们扩展我们的` add `函数,使其也接受数字——因此我们想要一个字符串或数字的数组。使用泛型表示法不会有问题:

//array-of-unions
// ✅ 和以前完全一样
function add(items: Array<string | number>, newItem: string | number)

1
然而,如果使用数组表示法,情况开始变得奇怪

//string-or-number-array
// ❌ 看起来还行,但实际不是
function add(items: string | number[], newItem: string | number)

1
如果你无法立即发现错误——那就是问题的一部分。它隐藏得如此之深,以至于需要一些时间才能看到。让我们通过实际实现该函数并查看我们收到的错误来帮助一下:

//not-assignable
// ❌ 为什么这不起作用 😭
function add(items: string | number[], newItem: string | number) {
return items.concat(newItem)
}

1
显示以下错误:

Type ‘string’ is not assignable to type ‘ConcatArray & string’ (2769)
TypeScript playground

1
2
3
4
5
6
7
对我来说,这一点毫无意义。为了解决这个谜题:这涉及到运算符优先级。[] 的绑定强度比 | 运算符更强,所以现在我们已经使 items 成为了 string OR number[] 类型。

我们想要的是:`(string | number)[]`,带括号,以使我们的代码正常工作。泛型版本没有这个问题,因为它无论如何都会使用尖括号将 Array 与其内容分开。

## keyof

让我们看一个相当常见的例子,我们有一个函数接受一个对象,并且我们还想将此对象的可能键的数组传递给同一个函数。如果我们想要实现类似 `pick` 或 `omit `的函数,我们就需要这个:

//pick
const myObject = {
foo: true,
bar: 1,
baz: ‘hello world’,
}

pick(myObject, [‘foo’, ‘bar’])

1
我们只想允许现有键作为第二个参数传递,那么我们该怎么做呢?使用 `keyof `类型操作符:

//pick-generic-notation
function pick<TObject extends Record<string, unknown>>(
object: TObject,
keys: Array
)

1
当我们使用泛型语法为数组时,一切都正常。但是如果我们将其更改为数组语法会发生什么?

//pick-array-notation
function pick<TObject extends Record<string, unknown>>(
object: TObject,
keys: keyof TObject[]
)

1
2
令人惊讶的是,没有错误,所以这应该是正确的。没有错误甚至更糟,因为这里存在错误——它只是不会显示在函数声明中——而是在我们尝试调用它时出现:

pick(myObject, [‘foo’, ‘bar’])

1
现在产生:

Argument of type ‘string[]’ is not assignable to parameter of type ‘keyof TObject[]’.(2345)

1
这条消息比之前的消息更加让人摸不着头脑。为什么我的键不是字符串?不知道 `keyof TObject[] `的合法输入是什么。只知道定义我们想要的正确方式是:`(keyof TObject)[]`

//fixed-array-notation
function pick<TObject extends Record<string, unknown>>(
object: TObject,
keys: (keyof TObject)[]
)

```

这些都是我在使用数组表示法时遇到的问题。