类型收窄
所谓的类型收窄, 就是当我们定义类型描述为了适应多种尝试使用,变量可能是多种类型,
此时在处理不同类型数据时,使用的方法只能是共性方法, 否则会有问题
例如:
此时我们需要自定一个左侧填充方法, 当用户传递数字类型,则填充数字个空格,如果传递字符串类型,则直接用字符串填充在目标字符串左侧
/**
* 左侧填充方法
* padding: 填充内容,如果是数字,将视为填充的空格数,如果是字符串,用字符串填充
* input :目标字符串,在input字符串左侧填充内容
*/
function padLeft(padding:number | string , input:string){
// 此时padding类型检查警告, 不能将 number | string 类型中string 分配给 number类型
return ' '.repeat(padding) + input
}
padLeft(3, 'hello')
示例中,repeat方法需要一个number类型的参数, 但是padding 有可能是string类型. 我们并没有明确的检查padding变量的类型.此时我们就要通过类型检查收窄padding变量的类型
function padLeft(padding:number | string , input:string){
// padding: number | string
if(typeof padding === 'number'){
// padding: number
return ' '.repeat(padding) + input
}
// padding: string
return padding + input
}
padLeft(3, 'hello')
添加类型检查后, 刚进入padLeft方法是,padding的类型为number | string, 在进行条件判断, 能进入if语句, padding的类型一定是number类型
在我们的if检查中, TypeScript 将
typeof padding === "number"
视为一种特殊形式的代码,成为类型保护TypeScript 遵循我们的程序, 并采用可能执行的路径来分析给定位置的值最具体的类型.
它着眼于这些特殊的检查(成为类型保护)和分配, 将类型精炼为比声明更具体类型的过程成为收窄
1. typeof类型守卫
JavaScript支持typeof 运算符, 用于检查值的类型. 返回一个特定的字符串:
- 'number'
- 'string'
- 'bigint' 就是新增的基本数据类型,用于创建大于253 -1 的数字, 因为这个数字是Number表示的最大数字
- 'boolean'
- 'symbol'
- 'undefined'
- 'object'
- 'function'
在TypeScript中, 检查typeof 返回值的类型是一种类型包含, 因为TypeScript编码了typeof不同值的操作方式, 所以他也知道JavaScript的一些怪癖, 例如, typeof不返回字符串'null', 在检查null值时返回的是'object'
看下面的示例
function printAll(strs:string | string[] | null ){
if(typeof strs === 'object'){
// string[]
for(let str of strs){
console.log(str)
}
}else if(typeof strs === 'string'){
// string
console.log(strs)
}else{
// null
// do nothing
}
}
示例代码的本意:是检查, 如果是string[]数组类型,则遍历打印数组的没一个值, 如果是字符串类型, 则直接打印, 如果是null类型什么都不做,
但是由于typeof 检查null的类型也是'object'的, 因此, 如果真的传入null参数, 程序将报错, 因为null遍历
2.真实性缩小
真实性缩小: 通过判断时的true,false,来缩小类型
但是真实的开发场景中我们可能会选择使用js 的隐式类型转换来进行进行条件判断
function getUserOnlineMessage(onlineNum:number){
if(onlineNum){
return `现在有${onlineNum}个用户在线`
}else{
return '无用户在线'
}
}
此时if添加语句会强制将数字类型的onlineNum
转为boolean类型进行判断.,
在JavaScript 总会判断为false的有
0
NaN
""空字符
On (bigint 零版本)
null
undefined
false
利用这些就可以有效的规避null 和 undefined情况
此时我们可以修改printAll 函数
function printAll(strs:string | string[] | null ){
// 先判断str 是否存在, 规避掉null的情况
if(strs && typeof strs === 'object'){
// string[]
for(let str of strs){
console.log(str)
}
}else if(typeof strs === 'string'){
// string
console.log(strs)
}else{
// null
// do nothing
}
}
此时我们通过检查strs是否为真来消除之前为null的错误,
3.相等缩小
相等缩小: 判断两个变量的值是否相等来缩小类型
TypeScript 还可以使用switch语句和相等检查, 例如===
,==
,!==
,!=
来缩小类型
如下实例
function example(x: string | number, y: string | boolean){
if(x === y){
console.log(x) // string
console.log(y) // string
}else{
console.log(x) // string | number
console.log(y) // string | boolean
}
}
鼠标移入x,y变量上, 会显示对应此刻变量类型
在实例中, 当检查x
和y
相等时, TypeScript知道他们的类型也必须相等,那是因为 string
类型是唯一可以同时使用的通用类型,TypeScript知道这一点,并且必须是第一个分支中的x
,'y', 它们同时都是string
类型
因此,在相等的分支中不用在做额外的类型处理, 就可以放心使用字符串方法
注意:
如果此时换成了
==
, TypeScript也同样会进行类型收缩, 同样会认为内部都是string
类型, 如果你直接使用字符串方法可能会带来问题原因在与
==
, 虽然TypeScript进行相同类型收缩, 但是JavaScript并不会, 例如0
和false
在使用==
判断时, 你会意外发现结果是true.
==
,!=
不仅会带来上述问题, 同样在文字值检查时,带来问题:
例如:
function example(x: number | null | undefined){
if(x != null){
console.log(x) // number
}else{
console.log(x)
}
}
在JavaScript中null
和undefined
在使用==
判断时,结果为true,
这里的本意是洗完过滤掉null
值, 但现在会潜在的过滤掉undefined
值,
因此建议在开发中尽量全部使用===
,!==
进行判断
4. in操作符收窄
in操作符收窄: 通过in
操作符的使用来精确类型
in操作符:JavaScript中用于确定一个对象是否带有某个名称的属性.
TypeScript将这一点视为缩小潜在类型的一种方式
例如,
在使用"value" in x
, 当"value"是字符串文字, 并且x
是一个联合类型,
此时, 条件为true
是, x
的类型将缩小具有'value'是可选或必需属性的类型,为false时, x
的类型缩小为具有可选或缺少value
属性的类型
interface Student{
reading: () => void
}
interface Worker {
working: () => void
}
function example(person:Student | Worker){
if("reading" in person){
return person.reading()
}
return person.working()
}
let student:Student = {
reading:() => {
console.log('我在读书')
}
}
example(student)
示例中, person
参数两个接口的联合类型, 如果此时 传递的参数 符合一个Student接口, 在运行接口中的reading
,方法是, TypeScript类型效验不通过, 警告Worker
接口中没有reading
方法
此时通过使用in
操作符, TypeScript会自动收窄参数类型, 即示例中, 当条件为真是,参数的类型只会保留Student
接口类型, 因此在使用reading
方法时,不会报类型错误
注意,可选属性将存在与两侧进行类型收窄, 也就是说, 可选属性的接口会在in
操作符两侧都保留收窄
interface Student{
reading: () => void
}
interface Worker {
working: () => void
}
interface Doctor{
reading?: () => void
}
function example(person:Student | Worker | Doctor){
if("reading" in person){
return person.reading()
}
return person.working()
}
示例中, person.working()
将会警告,Doctor接口没有working
方法, 也就是说, 虽然Doctor
有reading
方法, 但是是一个可选属性, 因此在false的情况下,person
参数也保留了Doctor
接口类型
5. instanceof 操作符收窄
JavaScript中的instanceof
运算符用来检查一个值是否是另一个值的实例, 更具体的说,是检查包含的原型链
示例:
function example(x: Date | String){
x.toUpperCase()
}
example('hello')
示例中x.toUpperCase()
警告Data
类上没有toUpperCase()
方法. 因此此时x是两个类的联合类型.
可以通过instanceof
运算符对类型进行收窄
function example(x: Date | String){
if(x instanceof String){
return x.toUpperCase()
}
return x.toUTCString()
}
example('hello')
此时将鼠标移入x, 在if条件为true 是, x的类型被收窄为只有String
, 条件为false是, 类型被收窄为只有Date
因此就可以在不同的类型场合下使用不同类型实例的方法
6. 分配类型
当我们给任何变量赋值时,TypeScript会自动查看赋值右侧并适当的缩小左侧的类型
例如:
let x = Math.random() > 0.5 ? 10 :'hello'
console.log(x)
// 此时x的类型 let x: string | number
// 当重新给变量x赋值时
x = 10;
console.log(x)
// x的类型 let x:number
// 重新赋值字符串
x = 'world'
console.log(x)
将鼠标移入console.log(x)
中x
变量上, 你会发现, 在初始赋值是,x
会被TypeScript自动分配了string | number
的联合类型,
当重新给x赋值为数字时,变量x的类型被收窄为number
类型, 重新赋值world
时,类型被 收窄为string
类型
这就是TypeScript会根据赋值语句右侧自动收窄左侧变量的类型
但是要注意, 如果此时给x赋值一个除了string | number
类型外的其他类型值将会出错
x = true;
// 报错: 不能将boolean类型的值赋值给string | number类型
因为在声明x变量时确定的类型,将会被TypeScript记录, TypeScript将始终根据声明变量时的类型来检查可分配行
示例中,声明x变量时,TypeScript 根据右侧的值分类了string | number类型
后续给x
赋值true
, true的类型为boolean
, 不符合可分配类型.
如果声明一个变量时没有赋初始值, 那么这个变量会被分配any
类型,
any
类型的变量可以赋值任何类型的值.
let x;
// let x: any
7. 控制流分析
流程控制分析: 就是TypeScript会更加流程来缩小类型, 并在流程结束后,能够从其余部分中删除不可访问的类型
基于这种流程控制分析, TypeScript在遇到类型保护和赋值时使用流分析来缩小类型, 当分析一个变量时, 控制流可以一次有一次的分裂和重新合并, 并可以观察该变量的每个点具有不同的类型
例如:
function example() {
let x: string | number | boolean;
// 节点一
x = Math.random() < 0.5;
console.log(x);
// let x: boolean
if (Math.random() < 0.5) {
// 节点二
x = "hello";
console.log(x);
// let x: string
} else {
// 节点三
x = 100;
console.log(x);
// let x: number
}
// 节点四
console.log(x)
// let x: string | number
}
示例中声明一个变量, 并声明可分配类型为string | number | boolean
联合类型
在节点一的时候, 给x赋值一个结果为boolean
类型的表达式, 此时x类型收窄为boolean
接下来进入流程控制:
在条件为true时,即节点二时,x被赋值为string
类型的值, 此时TypeScript 删除原来收窄的类型,重新收窄为string
类型
条件为false 时, 即节点三时, x被赋值为 number类型的值, 此时TypeScript重新收窄x变量的类型为number
类型
当结束流程控制时, TypeScript进行分析, 发现boolean
类型是通不过流程控制的, 在流程控制结束后,条件为true, x为string类型,条件为false ,x为number类型
此时TypeScript在流程控制结束后, 将x变量的类型收窄为string | number
, 删除通不过的boolean
类型
8. 类型谓词,
到目前为止,我们已经使用现有的 JavaScript 结构来处理缩小范围,但是有时您希望更直接地控制类型在整个代码中的变化方式。
定义用户类型包含, 需要定义一个返回类型为类型谓词的函数
const isStudent = (student: Student | Doctor):student is Student => {
return (student as Student).age !== undefined
}
这个函数的返回值写法student is Student
就是示例中的类型谓词, 谓词采用的形式parameterName is Type
, 其中parameterName
必须是当前函数签名中的参数名称
任何时候, isStudent
调用某个变量, 如果类型兼容, TypeScript将会将改变了缩小到特定的类型
示例
// 自定义类型包含,类型谓词
interface Student{
name: string,
age: number
}
interface Doctor{
name: string
phone: number
}
const students:Student[] = [
{name:'小明',age:18},
{name:'小红',age:22},
]
const doctors:Doctor[] = [
{name:'张三',phone:18612412414},
{name:'李四',phone:18612412415},
]
const persons = [...students,...doctors]
// 1.使用类型断言
const student = persons[1]
// console.log('student', (student as Student).age)
// 2.条件判断(类型收窄)
// if('age' in student){
// console.log(student.age)
// }
// 类型谓词
const isStudent = (student: Student | Doctor):student is Student => {
return (student as Student).age !== undefined
}
console.log(isStudent(student))
9. 识别联合
识别联合类型: 就是通过联合类型相同的属性来区分联合类型,
例如,假设我们尝试去圆形和正方形形状进行编码, 圆形记录半径, 正方形记录边长,我们将字段使用kind
来判断处理的形状
// 定义接口
interface Shape{
kind: 'circle' | 'square'
radius?:number
sideLength?: number
}
注意: 智力我们使用了字符串文字类型的联合,circle
和'square'用于区分圆形还是方形,
此时我们就可以编写一个getArea
函数,根据它是圆形还是方形来应用正确的逻辑,我们首先处理圆形
function getArea(shape:Shape){
return Math.PI * shape.radius ** 2
}
此时radius
属性可能没有定义, 就会导致 程序问题, 此时 我们就会想到对kind
属性进行适当的检查,
function getArea(shape:Shape){
if(shape.kind === 'circle'){
return Math.PI * shape.radius ** 2
}
}
此时又会带来另一个问题, radius
的值 可能是undefined, 此时可以使用非空断言(!)来告诉类型检查器radius
肯定存在
function getArea(shape:Shape){
if(shape.kind === 'circle'){
return Math.PI * shape.radius! ** 2
}
}
但这感觉并不理想, 因为我们不得不使用非空断言, 无论如何,我们都可能意外访问radius
,sideLength
中的任何一个,(因为在读取它们时可选属性始终存在),
这种编码的问题是类型检查器没有任何方法可以根据属性Shape
知道是否存在, 我们需要将我们所知道的信息传达给类型检查器, 考虑这一点,我们可以重新定义接口
interface Circle{
kind: 'circle'
radius: number
}
interface Square {
kind: 'square'
sideLength: number
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
}
当联合中的每个类型都包含具有文字类型的公共属性时,TypeScript 认为这是一个可区分的联合,并且可以缩小联合的成员范围。
在这种情况下,kind
是那个共同属性(这被认为是 的判别属性Shape
)。检查该kind
属性是否被"circle"
删除了所有Shape
没有typekind
属性的类型"circle"
。那缩小shape
到类型Circle
。