为什么学习TS
- TS自带静态类型检测,大幅减少低级错误
- 面向接口编程,减少代码重构,提升编码效率,更加适合中大型项目
- 找工作加分项,越来越多的主流框架使用TS开发,大厂招聘要求之一
TS的本质
TS是JS的超集,兼容JS的语法,可以理解为一个添加了类型注解的JS
TS的学习路线
- 入门, 学习基础类型
- 进阶, 学习类型守卫,类型兼容,工具类型等概念,及其在业务中的使用技巧
- 实战, 学习TS Config的配置,常见错误分享定位,浏览器和Nodejs端的开发应用,JS项目改造升级实践
TS 开发环境搭建
- IDE选择
- VS Code
- WebStorm
- 官方的playground
- 全局安装包
- npm i -g typescript
- npm i -g ts-node
- 初始化配置文件, tsc —init
- 编译ts为js, tsc
特别需要注意的是,VS Code 默认使用自身内置的 TypeScript 语言服务版本,而在应用构建过程中,构建工具使用的却是应用路径下 node_modules/typescript 里的 TypeScript 版本。如果两个版本之间存在不兼容的特性,就会造成开发阶段和构建阶段静态类型检测结论不一致的情况,因此,我们务必将 VS Code 语言服务配置成使用当前工作区的 TypeScript 版本
简单基础类型
- string
- number
- boolean
- null
- undefined
- symbol
静态类型检测
在编译时期,静态类型的语言可以准确地发现类型错误
ts的编译器通过对比检测的变量接受的值类型与注解的类型是否一致,从而检测类型是否存在错误
注意点:
- number 与bigint 虽然都表示数字,但类型不兼容
- Number、String、Boolean、Symbol 与 number、string、boolean、symbol 不等价,基本无用
练习题
请举例说明 ts(2322) 是一个什么错误?什么时候会抛出这个错误?
例子:
let word: string = 'I love you'
word = 100 // 不能将类型“string”分配给类型“number”
类型不兼容,赋值的类型与注解的类型不一致时抛出 ts(2322)的错误
复杂基础类型
- 数组,建议使用 [] 定义
- 避免与JSX的语法冲突
- 减少代码量
- 元组,可以限制数组元素的个数和类型, 适用于多返回值的场景
- any, 任意类型可绕过静态类型检测
- any类型在对象调用链中会进行传导,即any类型的属性类型都是any
- 应尽量避免使用any, 并开启禁用隐式的any设置
- unknown, 它主要用来描述类型不确定的变量
- 任意类型可以赋值给unknown, 但 unknown类型的值只能赋值给unknown或any
- 所有缩小类型手段对unknown有效,直接调用unknown类型的变量上的方法报错
- void, undefined, null, 在实际开发中使用strict模式基本废柴
- never, 表示永远不会有返回值的类型,
- 用于 ThrowError 或者死循环
- 它是所有类型的子类型
- 在为 false 的类型守卫条件判断下,变量的类型将缩小为 never
- 接口中使用never定义属性,等同于禁止写接口下特定的属性(readonly)
- object 类型,表示非原始的类型,实际开发中无用武之地,使用场景是用来表示 Object.create 的类型
- 类型断言,让TypeScript 按照我们的方式做类型检查
- xxx as type
- 类型断言的操作对象必须满足某些约束关系, 父子、子父类型之间可以使用类型断言进行转换
- 非空断言 !,它可以用来排除值为 null、undefined 的情况, 但一般建议使用类型守卫代替
练习题
类型断言需要满足什么约束条件?
操作对象必须满足包含关系,如父子,子父之间可以进行类型断言
字面量类型、类型推断、类型拓宽和类型缩小
- 类型推断
- ts的类型标注在变量之后,好处是编译器可以通过代码所在的上下文推导变量的类型
- ts 中具有初始化值的变量,有默认值的函数参数,函数的返回类型都可以通过上下文推断出来
- 上下文推断
- 字面量类型,是集合类型的子类型
- 数字字面量类型
- 字符串字面量类型
- 应用于联合类型, 如 type Direction = ‘up’ | ‘down’
- 把函数的参数限定为更具体的类型,提升程序的可读性
- 布尔字面量类型
- 类型拓宽
- 所有通过let/var定义的变量,函数的形参,对象的非只读属性,如果满足指定初始值未显示添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型的拓宽后的类型
- 对null和undefined的类型进行拓宽,满足条件a, 推断类型为any
- 类型缩小,通过某些操作将变量的类型由一个宽泛的集合缩小到较小的集合
- 使用类型守卫
- 字面量类型的等值判断 ===
- 流程控制语句,
- const 声明的常量类型为缩小
练习题
涉及字面量的类型推断都有什么规则?
- let/var 声明的字面量会拓宽类型,
- const 声明的字面量会类型缩小
- const 声明的对象变量会自动推断为对应类型,可以使用 as const 收窄,让每个属性变成只读的固定类型
函数类型:返回值类型和参数类型
- 返回值类型
- 未显示声明,返回值类型是void
- 显示声明返回值类型为undefined会报错,改成void即可
- 可缺省和可推断的返回值类型
- 它可以缺省和推断出来
- 但是有些特例需要显示声明返回值类型, 如 Generator
- 参数类型
- 可选参数和默认参数
- 默认参数类型必须是参数类型的子类型
- 剩余参数
- 可选参数和默认参数
- this
- 声明this的类型,只需要在函数的第一个参数中声明this指代的对象类型即可
- 显示注解函数中的this类型,它表面上占据了第一个形参的位置,但并不意味函数多了一个参数,ts在编译为js后,伪参数this会被删除
- 函数重载
- 同一个函数可以有不同的类型的参数和返回值类型,它可以更精确的描述参数与返回值类型约束关系
- ts 会从上到下查找函数重载列表中与入参类型匹配的类型,并优先使用第一个匹配的重载定义
- 类型谓词
- 添加到返回值类型
- 参数 + is + 类型,使类型缩小
- 实现自定义类型守卫
练习题
- 如何注解函数中 this 的类型?
只需要在函数的第一个参数中声明this类型即可
- 函数类型重载的匹配顺序是什么?
会从上到下查找函数重载列表中与入参类型匹配的类型,并优先使用第一个匹配的重载定义
类类型:使用类型化的面向对象编程
- 类,实际业务中,任何实体可以被抽象为一个使用类表达的类似对象的数据结构,既包含属性又包含方法
- 继承,
- 使用extends关键字,
- 派生类如果包含一个构造函数,则必须在构造函数中调用super方法
- 修饰符
- 公共, public 修饰的是在任何地方可见、公有的属性或方法
- 私有,private 修饰的是仅在同一类中可见、私有的属性或方法
- 受保护,protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法
- 只读修饰符, readonly 修饰类的属性不被更改
- 存取器, getter/setter
- 实现对类属性访问的拦截,实现特定的访问逻辑
- 静态属性,只存在于类上,把与类相关的常量、不依赖实例 this 上下文的属性和方法定义为静态属性,从而避免数据冗余,进而提升运行性能
- 抽象类, 它是一种不能被实例化仅能被子类继承的特殊类
- 接口类, 通过定义类的接口 让类去implements 从而约束类,但接口只能定义类的成员属性
- 类的类型,
- 它的类型的名字就是类名,表示类实例的类型,
- 在定义类的时候,我们声明的除构造函数外所有属性、方法的类型就是这个特殊类型的成员
练习题
public、private、protected 属性的区别是什么?
public, 内部、子类、外部均可访问
private, 内部可以访问
protected, 内部、子类可以访问
接口类型与类型别名
接口类型、或类型别名的作用是将内联类型抽离出来,从而实现类型可复用
- 接口类型
- TypeScript 对对象的类型检测遵循一种被称之为“鸭子类型”(duck typing)或者“结构化类型(structural subtyping)”的准则, 即只要两个对象的结构一致,属性和方法的类型一致,则它们的类型就是一致的
- 使用interface定义
- 可缺省属性?
- 只读属性,readonly
- 定义函数类型,而不包含函数的实现
- 索引签名
- 数字索引签名
- 字符串索引签名
- 数字作为对象索引时,它的类型既可以与数字兼容,也可以与字符串兼容
- 属性可以与索引签名进行混用,但是属性的类型必须是对应的数字索引或字符串索引的类型的子集,否则会出现错误提示
- 继承和实现,
- 继承使用extends , 只能使用兼容的类型覆盖继承的属性
- 实现使用implements
- 类型别名,
- 通过type关键字
- 支持组合类型,交叉类型,接口类型无法覆盖
- Interface 与 Type 的区别
- 在大多数的情况下使用接口类型和类型别名的效果等价
- 重复定义的接口类型,它的属性会叠加,这个特性使得我们可以极其方便地对全局变量、第三方库的类型做扩展
练习题
接口类型和类型别名的区别是什么?
高级类型之联合类型和交叉类型
- 联合类型,
- ‘|’ 操作符表示
- 接口类型联合,访问个别成员的特有属性和方法,需要使用基于in操作符的类型守卫
- 交叉类型
- ‘&’ 操作符表示, string & number 结果是 never
- 合并接口类型,将多个接口合并成一个类型
- 合并的多个接口类型存在同名属性
- 同名属性不兼容,string & number 合并后就是never
- 同名属性兼容,number & (number的子类型 | 数字字面量类型),合并后是两者类型的子类型
- 合并的多个接口类型存在同名属性
- 合并联合类型,为一个交叉类型,可以理解为合并联合类型求交集
- 联合,交叉组合
- 联合操作符 | 优先级 低于 交叉操作符 &
- 可以通过使用小括弧来调整操作符的优先级
- 类型缩减
- string 原始类型 和 string 字面量类型 联合,类型缩减成string了, ‘string’ | string 类型就是string
- 控制类型缩减,让IDE提示,只要给父类型添加 ‘& {}’, type size = ‘big’ | ‘small’ | string & {}
- 联合类型的成员是接口类型,如果满足其中一个接口的属性是另外一个接口属性的子集,这个属性也会类型缩减
练习题
在联合类型中,类型缩减的规则是什么?
联合类型中,如果类型兼容,类型会缩减为二者的父类型, ‘abc’ | string 类型缩减为 string
类型守卫
JavaScript 作为一种动态语言,意味着其中的参数、值可以是多态(多种类型)。因此,我们需要区别对待每一种状态,以此确保对参数、值的操作合法
- 类型守卫的作用
- 触发类型缩小
- 类型集合中的不同成员。
区分联合类型
- switch
- 字面量恒等 ===
- typeof, 适用于联合类型不可枚举
- instanceof, 适用于联合类型的成员为类
- in, 适用于联合类型的成员包含接口类型(对象),并且接口之间的属性不同
- 自定义类型守卫, 使用类型谓词is
区分枚举类型
- 枚举和其他任何枚举、类型都不可比较,除了数字枚举可以与数字类型比较之外;
- 数字枚举极其不稳定
- 失效的类型守卫
- 某些类型守卫在泛型函数中不能缩小类型
练习题
如何区分不同的接口对象类型
- in 操作符
- 自定义类型守卫