第十节: TypeScript 类型收窄

类型收窄

所谓的类型收窄, 就是当我们定义类型描述为了适应多种尝试使用,变量可能是多种类型,

此时在处理不同类型数据时,使用的方法只能是共性方法, 否则会有问题

例如:

此时我们需要自定一个左侧填充方法, 当用户传递数字类型,则填充数字个空格,如果传递字符串类型,则直接用字符串填充在目标字符串左侧

/**
 * 左侧填充方法
 * 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 运算符, 用于检查值的类型. 返回一个特定的字符串:

  1. 'number'
  2. 'string'
  3. 'bigint' 就是新增的基本数据类型,用于创建大于253 -1 的数字, 因为这个数字是Number表示的最大数字
  4. 'boolean'
  5. 'symbol'
  6. 'undefined'
  7. 'object'
  8. '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变量上, 会显示对应此刻变量类型

在实例中, 当检查xy相等时, TypeScript知道他们的类型也必须相等,那是因为 string类型是唯一可以同时使用的通用类型,TypeScript知道这一点,并且必须是第一个分支中的x,'y', 它们同时都是string类型

因此,在相等的分支中不用在做额外的类型处理, 就可以放心使用字符串方法

注意:

如果此时换成了==, TypeScript也同样会进行类型收缩, 同样会认为内部都是string类型, 如果你直接使用字符串方法可能会带来问题

原因在与== , 虽然TypeScript进行相同类型收缩, 但是JavaScript并不会, 例如0false在使用== 判断时, 你会意外发现结果是true.

==,!=不仅会带来上述问题, 同样在文字值检查时,带来问题:

例如:

function example(x: number | null | undefined){
    if(x != null){
        console.log(x) // number
    }else{
        console.log(x)
    }
}

在JavaScript中nullundefined在使用==判断时,结果为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 方法, 也就是说, 虽然Doctorreading方法, 但是是一个可选属性, 因此在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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容