在TypeScript中文网接口一章中,对于函数传参为什么将一个对象赋值给一个另一个变量,再将变量传入函数就可以绕过编译器检查 的原因只给了一个简单的回答:"因为 squareOptions不会经过额外属性检查,所以编译器不会报错".解释的如此简单,让人迷糊。本文将对这个问题进行探究。

缘起

问题源于TypeScript中接口一章额外的类型检查一节关于最后一种绕过编译器检查的描述

原问题请查看TypeScript绕过编译器检查的一点困惑

先看示例代码

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
  let newSquare = {color: "white", area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

[标注1]
createSquare({ color: 'red', width: 100, height: 12 });    // 错误

let param = { height: 200, width: 100, color: 'red' };

createSquare(param);    // 正确

[标注2]
let param = {}

createSquare(param)  // 正确

let param1 = { height: 200 }

createSquare(param1)  // 错误

let param2 = { height: 200 } as SquareConfig

createSquare(param2)  // 正确

let param3 = { height: 200, color: "red" }

createSquare(param3)  // 正确

原文中对 为什么将一个对象赋值给一个另一个变量,再将变量传入函数就可以绕过编译器检查 的原因只给了一个简单的回答:"因为 squareOptions不会经过额外属性检查,所以编译器不会报错".解释的如此简单,让人迷糊。

这里引出两个问题:

1.看 标注1 代码。为什么将一个对象赋值给一个另一个变量,再将变量传入函数就可以绕过编译器检查。而直接将对象作为参数传递到函数就会引起错误?

2.看 标注2 代码。对比来看,为什么传入 param1 参数就会引起错误?

解题思路

首先看代码中的函数

function createSquare(config: SquareConfig): {color: string; area: number} {
  ...
}

函数要求我们传入的参数是 SquareConfig 类型的。

函数参数{color: "red", width: 100 }可以定义为合法的 SquareConfig 类型,所以是正确的

函数参数{color: "red", width: 100, height: 100 }不可以定义为合法的 SquareConfig 类型,所以引起错误

对上面的描述感觉迷糊的话,请接着往下看...

再看下面的代码

const test1:SquareConfig = {  // 正确
  color: "red",
  width: 100
}

const test2:SquareConfig = {   // 出错
  color: "red",
  width: 100,
  height: 100
}

代码很简单,也很容易明白为什么。SquareConfig 只定义 color 和 width, 传入 height 肯定出错。。

function createSquare(config: SquareConfig)括号中实际进行了一次定义变量的过程,相当于如下代码:

const t: SquareConfig = config
function createSquare(t){
  ...
}

所以,当函数中传参 { color: 'red', width: 100, height: 12 }

定义变量都没走通的话,那肯定会报错啦

那为什么将一个对象赋值给一个另一个变量,再将变量传入函数就正确了呢?接着往下看

其实就是 object 转换为 SquareConfig 的问题。具体的内容还需要学习一下 类型兼容 那一章节,没看到那里,就先不具体聊了。

let a1 = {}
let t1: SquareConfig = a1   // 正确
createSquare(t1)

let a2 = { width: 10}
let t2: SquareConfig = a2   // 正确
createSquare(t2)

let a3 = { height: 10 }
let t3: SquareConfig = a3   // 错误
createSquare(t3)

let a4 = { width: 10, color: "red"}
let t4: SquareConfig = a4   // 正确
createSquare(t4)

let a5 = { width: 10, color: "red", height: 10}
let t5: SquareConfig = a5   // 正确
createSquare(t5)

转化要求 类型兼容。object 与 SquareConfig 类型兼容,所以可以转化。转化成功与否在于 object 中是否有与 SquareConfig 公共项。

至此就解释通了 标注1 和标注2 的问题啦.

总结

研究过程中也是走了一些弯弯的,开始时以为是在 编译器在定义函数参数那里自动默认进行了 类型推断 ,但后来才发现 类型推断的话,有很多地方解释不通。比如下面这一段代码,后来在热心大佬的指导下,才明白了为什么。

let param1 = { height: 200 }

createSquare(param1)  // 错误

let param2 = { height: 200 } as SquareConfig

createSquare(param2)  // 正确

实际写代码的过程中并不推荐使用这种 将一个对象赋值给一个另一个变量,再将变量传入函数 这种方式来达到向 函数内部传接口没定义的参数的目的。更推荐的写法如下所示

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}

加入[propName: string]: any; 表示接受任意类别参数。这样怎么传都是正确的,没有了上面那些"坑"