大家好,我卡颂。
有没有同学 学TS
的步骤和我一样:
-
先看
TS
文档(或各种入门教材),学学各种类型的定义 -
为现有项目中的
JS
代码增加类型 -
随着增加的类型越来越多,类型报错越来越多,不得已改为
any
类型,或者增加// @ts-ignore
注释
最后,使用TS
的成本(改各种类型报错耽误的时间)超过了收益(TS
带来的类型安全),TypeScript
也学成了AnyScript
。
上述历程我反复经历了两次。痛定思痛,决定系统学一遍TS
。
经过这次系统学习,我终于明白我为什么总学不好TS。希望这篇文章对和我有同样经历的同学有帮助。
学不好的原因
想必你听过一句话 ------ TS是JS的超集 。这句话本身是没错的,TS
在JS
的基础上扩展了类型系统与语法。
但如果我们以这句话为基础开始学习TS
,很容易形成一个惯性:以JS
为起点,逐步学习TS
知识。
也就是下图中从红圈(JS)逐渐向外学习(蓝圈),目标是最终覆盖绿圈(TS)。
从这个思路出发的学习步骤就是我们开篇提到的学习步骤。
按这个步骤学习的问题出在哪呢?当我们只把TS
看作JS
超集时,会忽略TS本身就是一门语言 这一事实。作为一门语言,TS
有自己的语法规范,与JS
相比:
-
TS
作为语言,操作的单位是类型 ,语法规范定义的是类型之间的操作逻辑,工作在编译时 -
JS
作为语言,操作的单位是变量 ,语法规范定义的是变量之间的操作逻辑,工作在运行时
如果我们只从JS
出发,是可以理解TS
与JS
兼容的部分(类型部分)。但不兼容的部分(TS
作为语言本身的语法规则)会成为我们进阶路上的绊脚石。
一个例子
举个例子,下面三个都是TS
中合法的类型:
-
object
:对应引用类型 -
{}
:空对象字面量对应的类型 -
Object
:Object
构造函数对应的类型
请问下面三个类型别名的结果是什么?
extends
关键字在条件类型语句a extends b ?
中用于判断a
是否是b
或b
的子类型
ts
type r0 = {} extends object ? true : false;
type r1 = object extends Object ? true : false;
type r2 = {} extends Object ? true : false;
即使没有TS
经验,从JS
语法出发,也能得到答案:
-
{}
是对象字面量,肯定属于对象类型的子类型,所以r0
为true
-
Object
处于JS
原型链的顶端,所有对象类型肯定是他的子类型,所以r1
为true
-
有了前两个结果,
r2
显然也为true
为什么没有TS
经验也能得出正确结果呢?因为TS
在类型方面是兼容JS
的。我们从JS
角度出发就能得到正确的TS
结果(注意上述r0~r2
的结果都是编译时由TS
计算出的)
但是,如果我们不学习TS
作为语言本身的规则,理解下面代码时就会产生困惑(我们将上述三段代码中extends
前后的类型调换下,得到的结果仍然都为true
):
ts
type r0 = object extends {} ? true : false;
type r1 = Object extends object ? true : false;
type r2 = Object extends {} ? true : false;
从JS
出发是很难理解这个结果的。要理解他,我们需要从TS
出发。
类型与类型系统
在JS
中我们定义不同变量后,可以按照语法规则对变量进行不同操作:
js
const num1 = 1;
const num2 = 2;
// 对变量的操作逻辑
console.log(num1 + num2); // 3
同样,在TS
中,我们定义不同类型后,也能按照语法规则对类型进行不同操作:
ts
type A = 1;
type B = 2;
// 对类型的操作逻辑
type C = A | B; // 1 | 2
TS
的语法规则被称为结构化类型系统 ,与JS
类比如下:
在TS
中,类型 与结构化类型系统 的关系可以用我们中学学到的集合的概念来类比,其中:
-
类型 是一类值的集合,比如
number
是数字字面量的集合,interface A
是满足接口A
规范的对象的集合 -
结构化类型系统 是集合之间兼容性判断的规则,比如怎么判断交集、怎么判断并集、怎么判断差集?
具体来讲,结构化类型 又叫鸭子类型,这是编程中一个很常见的术语,即 ------ 如果一只动物看起来像鸭子,叫起来像鸭子,走起来像鸭子,那他就是鸭子。
同样,结构化类型系统 在判断两个类型是否存在父子类型关系 时,也是通过对象成员是否有相同结构来判断的。
比如在下面代码中,我们定义Cat
与Dog
类型,以及接收Cat
类型参数的feedCat
函数。在调用feedCat
时,传入Dog
的实例并不会报错:
ts
class Cat {
eat() {}
}
class Dog {
eat() {}
}
function feedCat(cat: Cat) {}
feedCat(new Dog) // 不会报错
这是因为Cat
与Dog
的成员结构一致(都只包括返回值一致的eat
方法)。根据鸭子类型 ,既然成员结构一致,那Cat
与Dog
就是同类,所以feedCat
不会报错。
与结构化类型系统 (鸭子类型)相对的是指称类型系统 。在指称类型系统 中,类名必须一致才会被判定为同类,类之间必须有明确的继承关系(extends
)才会被判定为父子关系。
回到我们的代码:
ts
type r0 = object extends {} ? true : false;
type r1 = Object extends object ? true : false;
type r2 = Object extends {} ? true : false;
{}
代表一个没有任何成员的对象 。那么,换句话说,任何有成员的对象都能在{}
的基础上延伸出来,比如下面的接口A
可以看作是在{}
的基础上增加了name
属性:
ts
interface A {name: string}
所以,根据结构化类型系统 ,{}
是任何对象的父类,所以r0
、r2
(Object
是构造函数,函数也属于对象)为true
。
实际上,任何基础类型都有对应的包装类型,比如:
-
number
对应Number
-
string
对应String
-
boolean
对应Boolean
包装类型都是对象,所以{}
也是任何基础类型的父类(鸭子类型),即:
ts
type r3 = 1 extends {} ? true : false; // true
type r4 = 'hello' extends {} ? true : false; // true
type r5 = true extends {} ? true : false; // true
对于r1
,上面提到,Object
是构造函数,函数也属于对象,所以是object
的子类。
总结
TS
的出现为JS
带来静态分析能力。从这个角度看,TS
是兼容JS
的。所以从JS
出发学习TS
,在初期不会有很大阻力。
但是,TS
本身也是一门语言,这门语言的操作对象是类型,语法规则叫结构化类型系统。
所以,当我们想深入使用TS
时,必然会触碰TS
语言本身的规则,此时我们需要从TS
出发学习。
只有这样,才能真的学懂、用好TS
。
最后推荐下林不渡的《TypeScript 全面进阶指南》小册,讲的通俗易懂,是不错的教程。