为什么要兼容?

因为实际工作中,往往无法类型一致

假设我们现在需要设计一个接受参数为 一个对象包涵3个属性 的函数,但实际数据缺拥有更多属性,我们很容易写出以下代码:

1
2
3
4
5
6
7
const data = {
a: 1, b: 2, c: 3, d: 4
}

const newData = lodash.pick(data, ['a', 'b', 'c'])

fn(newData) // 假设 fn 函数 只需要 a, b, c

但是实际上我们并不会这么写,对于 JS程序员来说为什么要这么麻烦呢,虽然你只需要三个属性,我多传你几个,你不用不就好了,所以一般我们都会着那么写:

1
2
3
4
5
const data = {
a: 1, b: 2, c: 3, d: 4
}

fn(data)

所以 TS 需要适应这种习惯,他不能去纠正别人,如果故意把别人的习惯打破,那就很难去做推广

什么是类型兼容?

简单来说就是你就有的,我都有,则我们代替你,即:
y 有的,x都有,则 x 兼容 y

基本类型的兼容

1
2
3
type A = string | number

const a: A = 'hi'

此时,字符串 ‘hi’范围更小缺兼容了范围更大的,,似乎有点违背上面的理论,此时我们用集合的方式来理解会更好

结论:子集当然可以赋值给父集

普通对象的兼容

1
2
3
4
5
6
7
8
9
10
11
12
type Person = {
name: string;
age: number;
}

const user = {
name: 'frank',
age: 18,
id: 1,
email: 'qq.com',
}
const p: P = user

如果用上面的结论,子集 => 父集,那么对于对象来说, Person 和 user 那个范围更小呢?

其实通过逻辑推断来说,user 是更小的,因为属性多就是限制条件多,满足条件的就少,其实还是满足上述结论的,

但是为了方便记忆,我们可以得出对象的结论:属性多的可以赋值给属性少的

函数的兼容

相比下来函数会更复杂一点,函数不仅有属性,还包括参数和返回值

参数个数

直接上代码测试:

1
2
3
4
5
6
7
8
9
const 接受一个参数的函数 = (n :number) => {
console.log(n)
}
const 接受两个参数的函数 = (n: number, s: string) => {
console.log(n, s)
}

接受一个参数的函数 = 接受两个参数的函数 // TODO: OK
接受两个参数的函数 = 接受一个参数的函数 // TODO: 报错

很容易得出结论:参数少的兼容参数多的

如果还没理解的话,可以结合实际来看,我们经常在函数传参的时候,只会少写参数个数,比如在 axios 的调用、事件的绑定等等

1
2
3
4
5
6
const button = document.getElementById('submit')
const fn = (e: MouseEvent) => console.log(e)

button.addEventListener('click', fn)
button.addEventListener('click', fn, false) // 相比上面,我们经常会少写参数
button.addEventListener('click', fn, true)

在 JS 眼中,参数少传,问题不大,所以少写参数是很常见的

当然也许有杠精会说,我多传参数也问题不大啊。那么再想想看,实际你会去多传参数么?!基本不会吧~

参数类型

直接上代码测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface MyEvent {
target: string;
}
interface MyMouseEvent extends MyEvent {
x: number;
y: number;
}

const listener = (e: MyEvent) => console.log(e.target)
const listenerMouse = (e: MyMouseEvent) => console.log(e.x, e.y)

listenerMouse = listener // TODO: OK
listener = listenerMouse // TODO: 报错

很容易得出结论:对参数要求少的兼容对参数要求多的,与普通对象的兼容相反

因为相比普通对象来说,函数的参数是要求别人的,所以和普通对象相反也能理解

我们再来看一个实际的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Event {
target: number;
}
interface MyMouseEvent extends Event {
x: number;
y: number;
}
function listenEvent(eventType: string, handler: (n: Event) => void) { // TODO: 注意这里,只能写 Event,因为这里有可能会出现鼠标事件,所以只能只能写一个范围更大的类型
/* ... */
}

// 我们希望这么用
listenEvent('click', (e: MyMouseEvent) => console.log(e.x, x.y)) // TODO: 报错
// 但只能这么用
listenEvent('click', (e: Event) => console.log((e as MyMouseEvent).x, (e as MyMouseEvent).y))

// 还可以这么用
listenEvent('click', ((e: MyMouseEvent) => console.log(e.x, x.y)) as (e: Event) => void)

但是可以通过 TS 的配置让上面的代码正常运行

1
2
3
4
5
6
// tsconfig.json
{
"compilerOptions": {
"strictFunctionTypes": false
}
}

特殊类型

直接上文档

加餐

顶类型和底类型

这两个概念不是 TS 的内容,但却是类型系统的知识

先上理论:底部类型可以赋值给顶部类型,用一张图来表示:

写在最后

虽然推论了这么多,但是你其实都不用记忆,什么小的兼容大的,少的兼容多的…

因为实际一使用,TS 就会你报错提示你