Typescript map 类型踩坑

286 阅读3分钟

Typescript map 类型踩坑

map 是日常开发中常用的数组方法,用于根据已有的数组生成新的数组,下面来看一个例子。

在示例代码中,要将原对象数组中的每个对象的 module 字段做额外处理。

image

interface Configs {
	module: string
  tabTitle: string
}
const oldConfigs:Configs[] = [{ tabTitle: 'old', module: '1' }]

const newConfigs = oldConfigs.map((oldConfig) => {
  const module = oldConfig.module + '$'
  return {
    ...oldConfig,
    module
  }
})

可以看到 TS 是没有报错的。并且 newConfigs 也顺利的推到出了类型。

image

我们还可以通过标注 map 函数的泛型,让类型推导更准确。

image

这样就万事大吉了吗?

假设实际编码时,左右手抢拍,module 打成了 moduel,类型会提示报错吗?

image

答案是不会,TS 并没有提示,这样原来想要的功能也会失效。

首先看下 map 的类型定义。当我们标准泛型为 Configs 时,map 的第一个参数的类型(再本例中)应为:(value: Configs, index: number, array: Configs[]) => Configs

image

没有报错说明代码中写的匿名箭头函数是符合这一类型的。可以用“鸭子类型”来简单的理解这一现象。

由于返回的对象中包含 Configs 的所有字段,那么它就是 Configs,即使它还多了其他字段。

深挖一下 - 子类型编程中的协变和逆变

TS 为什么会是这样的行为呢?为什么会认为是类型安全的? 从上面的例子可以看出类型安全不等于业务安全。

你可以直接看这篇文章

以下是个人看完之后的理解:

把上文例子中的 map 函数类型再简化为 map(狗 -> 狗): 狗[] (因为例子只是修改了值,所以类型上还是 dog -> dog)

除了传递 dog -> dog 这个绝对正确的类型外,还可能发生以下几种情况:

  1. 大黄 -> 大黄

    • 类型不安全,map 函数传给它的是狗,可能不具备大黄的能力,所以大黄 -> 大黄运行可能会报错。
  2. 大黄 -> 动物

    • 类型不安全,map 函数会操作返回值 狗,返回的是动物可能不具备狗的能力,所以 map 函数运行可能会出错
  3. 动物 -> 大黄

    • 类型安全,map 传进来的狗拥有动物的所有能力,动物 -> 大黄不会出错;大黄是狗的子类,map 操作也不会出错。
  4. 动物 -> 动物

    • 类型不安全,同 2。

综上,当函数的返回值是目标类型(狗)或是目标类型子类(大黄)时,类型是安全的。而参数却恰恰相反,参数是目标类型(狗)或是目标类型父类(动物)时,才是类型安全

用合适的术语来描述这个奇怪的表现,可以说我们允许一个函数类型中,返回值类型是协变的,而参数类型是逆变的。返回值类型是协变的,意思是 A ≼ B 就意味着 (T → A) ≼ (T → B) 。参数类型是逆变的,意思是 A ≼ B 就意味着 (B → T) ≼ (A → T) ( A 和 B 的位置颠倒过来了)。

所以 TS “没错”。在 TS 这类类型系统中,类型安全不等于业务安全

如何规避?

map 处理数据时,不要直接展开原对象,而是通过解构赋值获取“剩余参数”,并展开剩余参数。

image

相关信息

TS 对​**对象字面量**​做了额外的检查,必须完全符合类型定义才正确,而变量则是鸭子类型校验。

image