为什么我总学不好TS?

大家好,我卡颂。

有没有同学 学TS的步骤和我一样:

  1. 先看TS文档(或各种入门教材),学学各种类型的定义

  2. 为现有项目中的JS代码增加类型

  3. 随着增加的类型越来越多,类型报错越来越多,不得已改为any类型,或者增加// @ts-ignore注释

最后,使用TS的成本(改各种类型报错耽误的时间)超过了收益(TS带来的类型安全),TypeScript也学成了AnyScript。

上述历程我反复经历了两次。痛定思痛,决定系统学一遍TS。

经过这次系统学习,我终于明白我为什么总学不好TS。希望这篇文章对和我有同样经历的同学有帮助。

打包领取卡颂原创React教程、加入人类高质量前端群

学不好的原因

想必你听过一句话 ------ 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语法出发,也能得到答案:

  1. {}是对象字面量,肯定属于对象类型的子类型,所以r0为true

  2. Object处于JS原型链的顶端,所有对象类型肯定是他的子类型,所以r1为true

  3. 有了前两个结果,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 全面进阶指南》小册,讲的通俗易懂,是不错的教程。

相关推荐
卸任3 小时前
Vibe coding工具使用心得
前端·ai编程
brien94 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance4 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M4 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写4 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李4 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易5 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马5 小时前
前沿视界
前端
kyriewen5 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易5 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员