大家好,我卡颂。
有没有同学 学TS
的步骤和我一样:
- 先看
TS
文档(或各种入门教材),学学各种类型的定义 - 为现有项目中的
JS
代码增加类型 - 随着增加的类型越来越多,类型报错越来越多,不得已改为
any
类型,或者增加// @ts-ignore
注释
最后,使用TS
的成本(改各种类型报错耽误的时间)超过了收益(TS
带来的类型安全),TypeScript
也学成了AnyScript
。
上述历程我反复经历了两次。痛定思痛,决定系统学一遍TS
。
经过这次系统学习,我终于明白我为什么总学不好TS。希望这篇文章对和我有同样经历的同学有帮助。
免费领取卡颂原创React教程(原价359)、加入人类高质量前端群
学不好的原因
想必你听过一句话 —— 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
的子类型
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
):
type r0 = object extends {} ? true : false;
type r1 = Object extends object ? true : false;
type r2 = Object extends {} ? true : false;
从JS
出发是很难理解这个结果的。要理解他,我们需要从TS
出发。
类型与类型系统
在JS
中我们定义不同变量后,可以按照语法规则对变量进行不同操作:
const num1 = 1;
const num2 = 2;
// 对变量的操作逻辑
console.log(num1 + num2); // 3
同样,在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
的实例并不会报错:
class Cat {
eat() {}
}
class Dog {
eat() {}
}
function feedCat(cat: Cat) {}
feedCat(new Dog) // 不会报错
这是因为Cat
与Dog
的成员结构一致(都只包括返回值一致的eat
方法)。根据鸭子类型,既然成员结构一致,那Cat
与Dog
就是同类,所以feedCat
不会报错。
与结构化类型系统(鸭子类型)相对的是指称类型系统。在指称类型系统中,类名必须一致才会被判定为同类,类之间必须有明确的继承关系(extends
)才会被判定为父子关系。
回到我们的代码:
type r0 = object extends {} ? true : false;
type r1 = Object extends object ? true : false;
type r2 = Object extends {} ? true : false;
{}
代表一个没有任何成员的对象。那么,换句话说,任何有成员的对象都能在{}
的基础上延伸出来,比如下面的接口A
可以看作是在{}
的基础上增加了name
属性:
interface A {name: string}
所以,根据结构化类型系统,{}
是任何对象的父类,所以r0
、r2
(Object
是构造函数,函数也属于对象)为true
。
实际上,任何基础类型都有对应的包装类型,比如:
number
对应Number
string
对应String
boolean
对应Boolean
包装类型都是对象,所以{}
也是任何基础类型的父类(鸭子类型),即:
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 全面进阶指南》小册,讲的通俗易懂,是不错的教程。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。