TypeScript 类型检查和保护
类型检查机制
类型检查机制: TypeScript 编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。其作用是辅助开发,提高开发效率
类型推断
类型推断: 指的是不需要指定变量的类型(包括函数的返回值类型),TypeScript 可以根据某些规则自动地为其推断出一个类型
基础类型推断
具有初始化值的变量,或者有默认值的函数参数、函数返回的类型等都可以推断出来。
let a = 1 // 推断为 number
let b = [1] // 推断为 number[]
let c = (x = 1) => x + 1 // 推断参数 x 的类型是 number 或者 undefined , 推断整个函数为 (x?: number) => number最佳通用类型推断
当需要从多个类型中推断出一个类型的时候,TypeScript 会尽可能的推断出一个兼容当前所有类型的通用类型
let d = [1, null]
// 推断为一个最兼容的类型,所以推断为(number | null)[]
// 当关闭"strictNullChecks"配置项时,null是number的子类型,所以推断为number[]上下文类型推断
以上的推断都是从右向左,即根据表达式推断,上下文类型推断是从左向右,通常会发生在事件处理中。
let 和 const 的类型推断区别
const 定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript 推断出它的类型直接由赋值字面量的类型决定,也是一种比较合理的设计。
let 定义的是可变更的变量,缺省显式类型注解时,变量的类型推断为赋值字面量类型的父类型,称为"Literal widening",也就是字面量类型的拓宽。这种设计也是符合编程预期的,以下面的代码为例,意味着我们可以给 str 和 num 任意值,只要类型是 string 和 number 的子集的变量。
类型守卫
JavaScript 作为一种动态语言,意味着其中的参数、值可以是多态(多种类型)。因此需要区别对待每一种状态,确保对参数、值的操作合法。
在 TypeScript 中,因为受静态类型检测约束,所以在编码阶段必须使用类似的手段确保当前的数据类型支持相应的操作。当然,前提条件是已经显式地注解了类型的多态。
上面的代码中,typeof、Array.isArray 条件判断就是类型守卫。类型守卫的作用在于触发类型缩小。实际上,它还可以用来区分类型集合中的不同成员,类型集合一般包括联合类型和枚举类型
联合类型的类型守卫
使用类型守卫来区分联合类型的不同成员,常用的类型守卫包括switch、字面量恒等、typeof、instanceof、in 和自定义类型守卫 这几种。
一般来说,如果可枚举的值和条件分支越多,那么使用 switch 就会让代码逻辑更简洁、更清晰;反之,则推荐使用字面量恒等进行判断。联合类型的成员如果是类,可以使用 instanceof。
通过类型谓词 is,可以封装自定义类型守卫。
失效的类型守卫
失效的类型守卫指的是某些类型守卫应用在泛型函数中时不能缩小类型,即失效了。例如 in 和 instanceof、类型谓词封装的自定义类型守卫在泛型类型缩小上是有区别的。
类型拓宽和类型缩小
Literal Widening
所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么他们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。
Type Widening
通过 let、var 定义的变量如果满足未显式声明类型注解且被直接赋予了 null 或 undefined 值,会对 null 和 undefined 的类型进行拓宽,推断出这些变量的类型是 any。
注意,上面没有说函数的形参,形参的类型不会进行拓宽
最后几行代码中出现的变量、形参的类型还是保持 null 或 undefined,没有拓宽成 any,也是符合预期的,这样可以让我们更谨慎的对待这些变量、形参。
Type Narrowing
在 TypeScript 中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是“Type Narrowing”。
例如可以使用类型守卫将函数参数的类型从 any 缩小到明确的类型。
类型断言
在确定自己比 TS 更准确的知道类型时,可以使用类型断言来绕过 TS 的检查,改造旧代码很有效,但是防止滥用。
可以使用 as 语法做类型断言,也可以使用尖括号 + 类型的格式做类型断言 <Bar>{},这两种方法虽然没有任何区别,但是尖括号格式会与 JSX 产生语法冲突,因此更推荐 as 语法。
类型断言的操作对象必须满足某些约束关系,否则我们将得到一个 ts(2352) 的错误,即从类型“原类型”到类型“目标类型”的转换是错误的,因为这两种类型不能充分重叠(可以简单理解为 as 只能转换父子类型),例如 1 as string。不过,any 和 unknown 这两个特殊类型属于万金油,因为它们既可以被断言成任何类型,反过来任何类型也都可以被断言成 any 或者 unknown。如果想强行断言不充分重叠的情况,可以先断言为 any 或 unknown,再断言为其他的。例如 1 as any as string
除了可以把特定类型断言成符合约束添加的其他类型外,还可以使用字面量 + as const”语法结构进行常量断言。
另外还有一种特殊非空断言,即在值(变量、属性)的后边添加 ! 断言操作符,它可以用来排除值为 null、undefined 的情况。对于非空断言,应该把它视作和 any 一样危险的选择,所以建议用类型守卫来代替非空断言。
类型兼容
当一个类型 Y 可以被赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y
X兼容Y:X(目标类型) = Y(源类型)
接口兼容
成员少的兼容成员多的
虽然成员少的可以兼容成员多的,但是如果直接把跟 Y 结构完全一样的对象字面量赋值给 x,则会提示一个 ts(2322)类型不兼容的错误,这就是对象字面量的 freshness 特性。 也就是说一个对象字面量没有被变量接收时,它将处于一种 freshness 新鲜的状态。这时 TypeScript 会对对象字面量的赋值操作进行严格的类型检测,只有目标变量的类型与对象字面量的类型完全一致时,对象字面量才可以赋值给目标变量,否则会提示类型错误。使用变量接收对象字面量或使用类型断言解除 freshness
函数兼容性
1、参数个数
1.1、固定参数
目标函数的参数个数一定要多于源函数的参数个数
Handler 目标函数,传入 test 的 参数函数 就是源函数
1.2、 可选参数和剩余参数
(1) 固定参数是可以兼容可选参数和剩余参数的
(2) 可选参数是不兼容固定参数和剩余参数的,但是可以通过设置"strictFunctionTypes": false 来消除报错,实现兼容
(3) 剩余参数可以兼容固定参数和可选参数
2、参数类型
2.1、 基础类型
2.2、 接口类型
接口成员多的兼容成员少的,也可以理解把接口展开,参数多的兼容参数少的。对于不兼容的,也可以通过设置"strictFunctionTypes": false 来消除报错,实现兼容
2.3、 函数类型
3、返回值类型
目标函数的返回值类型必须与源函数的返回值类型相同,或者是其子类型
4、函数重载
函数重载列表(目标函数)
函数的具体实现(源函数)
目标函数的参数要多于源函数的参数才能兼容
返回值类型不兼容
枚举类型兼容性
枚举类型和数字类型是完全兼容的
枚举类型之间是完全不兼容的
类的兼容性
和接口比较相似,只比较结构,需要注意,在比较两个类是否兼容时,静态成员和构造函数是不参与比较的,如果两个类具有相同的实例成员,那么他们的实例就相互兼容
私有属性
类中存在私有属性情况有两种,如果其中一个类有私有属性,另一个没有。没有的可以兼容有的,如果两个类都有,那两个类都不兼容。
如果一个类中有私有属性,另一个类继承了这个类,那么这两个类就是兼容的。
泛型兼容
泛型接口
泛型接口为空时,泛型指定不同的类型,也是兼容的。
如果泛型接口中有一个接口成员时,类型不同就不兼容了
泛型函数
两个泛型函数如果定义相同,没有指定类型参数的话也是相互兼容的
兼容性总结
结构之间兼容:成员少的兼容成员多的
函数之间兼容:参数多的兼容参数少的
类型保护机制
指的是 TypeScript 能够在特定的区块(类型保护区块)中保证变量属于某种特定的类型。可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。
实现 getLanguage 方法直接用 lang.helloJava 是不是存在,作为判断是会报错的
利用之前的知识可以使用类型断言解决
类型保护第一种方法,instanceof
类型保护第二种方法, in 可以判断某个属性是不是属于某个对象
类型保护第三种方法, typeof 类型保护,可以帮助我们判断基本类型
类型保护第四种方法,通过创建一个类型保护函数来判断对象的类型
类型保护函数的返回值有点不同,用到了 is ,叫做类型谓词
总结
不同的判断方法有不同的使用场景:
typeof:判断一个变量的类型(多用于基本类型)
instanceof:判断一个实例是否属于某个类
in:判断一个属性是否属于某个对象
类型保护函数:某些判断可能不是一条语句能够搞定的,需要更多复杂的逻辑,适合封装到一个函数内。
推荐阅读
Last updated