Typescript map 类型踩坑
map 是日常开发中常用的数组方法,用于根据已有的数组生成新的数组,下面来看一个例子。
在示例代码中,要将原对象数组中的每个对象的 module 字段做额外处理。
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 也顺利的推到出了类型。
我们还可以通过标注 map 函数的泛型,让类型推导更准确。
这样就万事大吉了吗?
假设实际编码时,左右手抢拍,module 打成了 moduel,类型会提示报错吗?
答案是不会,TS 并没有提示,这样原来想要的功能也会失效。
首先看下 map 的类型定义。当我们标准泛型为 Configs 时,map 的第一个参数的类型(再本例中)应为:(value: Configs, index: number, array: Configs[]) => Configs
没有报错说明代码中写的匿名箭头函数是符合这一类型的。可以用“鸭子类型”来简单的理解这一现象。
由于返回的对象中包含 Configs 的所有字段,那么它就是 Configs,即使它还多了其他字段。
深挖一下 - 子类型编程中的协变和逆变
TS 为什么会是这样的行为呢?为什么会认为是类型安全的? 从上面的例子可以看出类型安全不等于业务安全。
你可以直接看这篇文章
以下是个人看完之后的理解:
把上文例子中的 map 函数类型再简化为 map(狗 -> 狗): 狗[] (因为例子只是修改了值,所以类型上还是 dog -> dog)
除了传递 dog -> dog 这个绝对正确的类型外,还可能发生以下几种情况:
-
大黄 -> 大黄
- 类型不安全,map 函数传给它的是狗,可能不具备大黄的能力,所以大黄 -> 大黄运行可能会报错。
-
大黄 -> 动物
- 类型不安全,map 函数会操作返回值 狗,返回的是动物可能不具备狗的能力,所以 map 函数运行可能会出错
-
动物 -> 大黄
- 类型安全,map 传进来的狗拥有动物的所有能力,动物 -> 大黄不会出错;大黄是狗的子类,map 操作也不会出错。
-
动物 -> 动物
- 类型不安全,同 2。
综上,当函数的返回值是目标类型(狗)或是目标类型子类(大黄)时,类型是安全的。而参数却恰恰相反,参数是目标类型(狗)或是目标类型父类(动物)时,才是类型安全。
用合适的术语来描述这个奇怪的表现,可以说我们允许一个函数类型中,返回值类型是协变的,而参数类型是逆变的。返回值类型是协变的,意思是 A ≼ B 就意味着 (T → A) ≼ (T → B) 。参数类型是逆变的,意思是 A ≼ B 就意味着 (B → T) ≼ (A → T) ( A 和 B 的位置颠倒过来了)。
所以 TS “没错”。在 TS 这类类型系统中,类型安全不等于业务安全 。
如何规避?
map 处理数据时,不要直接展开原对象,而是通过解构赋值获取“剩余参数”,并展开剩余参数。
相关信息
TS 对**对象字面量**做了额外的检查,必须完全符合类型定义才正确,而变量则是鸭子类型校验。