第十節(jié): TypeScript 類型收窄

類型收窄

所謂的類型收窄, 就是當(dāng)我們定義類型描述為了適應(yīng)多種嘗試使用,變量可能是多種類型,

此時(shí)在處理不同類型數(shù)據(jù)時(shí),使用的方法只能是共性方法, 否則會(huì)有問(wèn)題

例如:

此時(shí)我們需要自定一個(gè)左側(cè)填充方法, 當(dāng)用戶傳遞數(shù)字類型,則填充數(shù)字個(gè)空格,如果傳遞字符串類型,則直接用字符串填充在目標(biāo)字符串左側(cè)

/**
 * 左側(cè)填充方法
 * padding: 填充內(nèi)容,如果是數(shù)字,將視為填充的空格數(shù),如果是字符串,用字符串填充
 * input :目標(biāo)字符串,在input字符串左側(cè)填充內(nèi)容
*/
function padLeft(padding:number | string , input:string){
    // 此時(shí)padding類型檢查警告, 不能將 number | string 類型中string 分配給 number類型
    return ' '.repeat(padding) + input
}
padLeft(3, 'hello')

示例中,repeat方法需要一個(gè)number類型的參數(shù), 但是padding 有可能是string類型. 我們并沒(méi)有明確的檢查padding變量的類型.此時(shí)我們就要通過(guò)類型檢查收窄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')

添加類型檢查后, 剛進(jìn)入padLeft方法是,padding的類型為number | string, 在進(jìn)行條件判斷, 能進(jìn)入if語(yǔ)句, padding的類型一定是number類型

在我們的if檢查中, TypeScript 將typeof padding === "number"視為一種特殊形式的代碼,成為類型保護(hù)

TypeScript 遵循我們的程序, 并采用可能執(zhí)行的路徑來(lái)分析給定位置的值最具體的類型.

它著眼于這些特殊的檢查(成為類型保護(hù))和分配, 將類型精煉為比聲明更具體類型的過(guò)程成為收窄


1. typeof類型守衛(wèi)

JavaScript支持typeof 運(yùn)算符, 用于檢查值的類型. 返回一個(gè)特定的字符串:

  1. 'number'
  2. 'string'
  3. 'bigint' 就是新增的基本數(shù)據(jù)類型,用于創(chuàng)建大于253 -1 的數(shù)字, 因?yàn)檫@個(gè)數(shù)字是Number表示的最大數(shù)字
  4. 'boolean'
  5. 'symbol'
  6. 'undefined'
  7. 'object'
  8. 'function'

在TypeScript中, 檢查typeof 返回值的類型是一種類型包含, 因?yàn)門ypeScript編碼了typeof不同值的操作方式, 所以他也知道JavaScript的一些怪癖, 例如, typeof不返回字符串'null', 在檢查null值時(shí)返回的是'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[]數(shù)組類型,則遍歷打印數(shù)組的沒(méi)一個(gè)值, 如果是字符串類型, 則直接打印, 如果是null類型什么都不做,

但是由于typeof 檢查null的類型也是'object'的, 因此, 如果真的傳入null參數(shù), 程序?qū)?bào)錯(cuò), 因?yàn)閚ull遍歷


2.真實(shí)性縮小

真實(shí)性縮小: 通過(guò)判斷時(shí)的true,false,來(lái)縮小類型

但是真實(shí)的開發(fā)場(chǎng)景中我們可能會(huì)選擇使用js 的隱式類型轉(zhuǎn)換來(lái)進(jìn)行進(jìn)行條件判斷

function getUserOnlineMessage(onlineNum:number){
    if(onlineNum){
        return `現(xiàn)在有${onlineNum}個(gè)用戶在線`
    }else{
        return '無(wú)用戶在線'
    }
}

此時(shí)if添加語(yǔ)句會(huì)強(qiáng)制將數(shù)字類型的onlineNum轉(zhuǎn)為boolean類型進(jìn)行判斷.,

在JavaScript 總會(huì)判斷為false的有

0 NaN ""空字符 On (bigint 零版本) null undefined false

利用這些就可以有效的規(guī)避null 和 undefined情況

此時(shí)我們可以修改printAll 函數(shù)

function printAll(strs:string | string[] | null ){
    // 先判斷str 是否存在, 規(guī)避掉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
    }
}

此時(shí)我們通過(guò)檢查strs是否為真來(lái)消除之前為null的錯(cuò)誤,


3.相等縮小

相等縮小: 判斷兩個(gè)變量的值是否相等來(lái)縮小類型

TypeScript 還可以使用switch語(yǔ)句和相等檢查, 例如===,==,!==,!=來(lái)縮小類型

如下實(shí)例

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
   }
}

鼠標(biāo)移入x,y變量上, 會(huì)顯示對(duì)應(yīng)此刻變量類型

在實(shí)例中, 當(dāng)檢查xy相等時(shí), TypeScript知道他們的類型也必須相等,那是因?yàn)?string類型是唯一可以同時(shí)使用的通用類型,TypeScript知道這一點(diǎn),并且必須是第一個(gè)分支中的x,'y', 它們同時(shí)都是string類型

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

注意:

如果此時(shí)換成了==, TypeScript也同樣會(huì)進(jìn)行類型收縮, 同樣會(huì)認(rèn)為內(nèi)部都是string類型, 如果你直接使用字符串方法可能會(huì)帶來(lái)問(wèn)題

原因在與== , 雖然TypeScript進(jìn)行相同類型收縮, 但是JavaScript并不會(huì), 例如0false在使用== 判斷時(shí), 你會(huì)意外發(fā)現(xiàn)結(jié)果是true.

==,!=不僅會(huì)帶來(lái)上述問(wèn)題, 同樣在文字值檢查時(shí),帶來(lái)問(wèn)題:

例如:

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

在JavaScript中nullundefined在使用==判斷時(shí),結(jié)果為true,

這里的本意是洗完過(guò)濾掉null值, 但現(xiàn)在會(huì)潛在的過(guò)濾掉undefined值,

因此建議在開發(fā)中盡量全部使用===,!==進(jìn)行判斷


4. in操作符收窄

in操作符收窄: 通過(guò)in操作符的使用來(lái)精確類型

in操作符:JavaScript中用于確定一個(gè)對(duì)象是否帶有某個(gè)名稱的屬性.

TypeScript將這一點(diǎn)視為縮小潛在類型的一種方式

例如,

在使用"value" in x, 當(dāng)"value"是字符串文字, 并且x是一個(gè)聯(lián)合類型,

此時(shí), 條件為true是, x的類型將縮小具有'value'是可選或必需屬性的類型,為false時(shí), 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參數(shù)兩個(gè)接口的聯(lián)合類型, 如果此時(shí) 傳遞的參數(shù) 符合一個(gè)Student接口, 在運(yùn)行接口中的reading,方法是, TypeScript類型效驗(yàn)不通過(guò), 警告Worker接口中沒(méi)有reading方法

此時(shí)通過(guò)使用in操作符, TypeScript會(huì)自動(dòng)收窄參數(shù)類型, 即示例中, 當(dāng)條件為真是,參數(shù)的類型只會(huì)保留Student接口類型, 因此在使用reading方法時(shí),不會(huì)報(bào)類型錯(cuò)誤

注意,可選屬性將存在與兩側(cè)進(jìn)行類型收窄, 也就是說(shuō), 可選屬性的接口會(huì)在in操作符兩側(cè)都保留收窄

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()將會(huì)警告,Doctor接口沒(méi)有working 方法, 也就是說(shuō), 雖然Doctorreading方法, 但是是一個(gè)可選屬性, 因此在false的情況下,person參數(shù)也保留了Doctor接口類型


5. instanceof 操作符收窄

JavaScript中的instanceof運(yùn)算符用來(lái)檢查一個(gè)值是否是另一個(gè)值的實(shí)例, 更具體的說(shuō),是檢查包含的原型鏈

示例:

function example(x: Date | String){
    x.toUpperCase()
}

example('hello')

示例中x.toUpperCase()警告Data類上沒(méi)有toUpperCase()方法. 因此此時(shí)x是兩個(gè)類的聯(lián)合類型.

可以通過(guò)instanceof運(yùn)算符對(duì)類型進(jìn)行收窄

function example(x: Date | String){
    if(x instanceof String){
        return x.toUpperCase()
    }
    return x.toUTCString()
}

example('hello')

此時(shí)將鼠標(biāo)移入x, 在if條件為true 是, x的類型被收窄為只有String, 條件為false是, 類型被收窄為只有Date

因此就可以在不同的類型場(chǎng)合下使用不同類型實(shí)例的方法


6. 分配類型

當(dāng)我們給任何變量賦值時(shí),TypeScript會(huì)自動(dòng)查看賦值右側(cè)并適當(dāng)?shù)目s小左側(cè)的類型

例如:

let x = Math.random() > 0.5 ? 10 :'hello'
console.log(x)
// 此時(shí)x的類型  let x: string | number

// 當(dāng)重新給變量x賦值時(shí)
x = 10;
console.log(x)
// x的類型  let x:number

// 重新賦值字符串
x = 'world'
console.log(x)

將鼠標(biāo)移入console.log(x)x變量上, 你會(huì)發(fā)現(xiàn), 在初始賦值是,x會(huì)被TypeScript自動(dòng)分配了string | number的聯(lián)合類型,

當(dāng)重新給x賦值為數(shù)字時(shí),變量x的類型被收窄為number類型, 重新賦值world時(shí),類型被 收窄為string類型

這就是TypeScript會(huì)根據(jù)賦值語(yǔ)句右側(cè)自動(dòng)收窄左側(cè)變量的類型

但是要注意, 如果此時(shí)給x賦值一個(gè)除了string | number類型外的其他類型值將會(huì)出錯(cuò)

x = true;
// 報(bào)錯(cuò): 不能將boolean類型的值賦值給string | number類型

因?yàn)樵诼暶鱴變量時(shí)確定的類型,將會(huì)被TypeScript記錄, TypeScript將始終根據(jù)聲明變量時(shí)的類型來(lái)檢查可分配行

示例中,聲明x變量時(shí),TypeScript 根據(jù)右側(cè)的值分類了string | number類型

后續(xù)給x賦值true, true的類型為boolean, 不符合可分配類型.

如果聲明一個(gè)變量時(shí)沒(méi)有賦初始值, 那么這個(gè)變量會(huì)被分配any類型,

any 類型的變量可以賦值任何類型的值.

let x;
// let x: any


7. 控制流分析

流程控制分析: 就是TypeScript會(huì)更加流程來(lái)縮小類型, 并在流程結(jié)束后,能夠從其余部分中刪除不可訪問(wèn)的類型

基于這種流程控制分析, TypeScript在遇到類型保護(hù)和賦值時(shí)使用流分析來(lái)縮小類型, 當(dāng)分析一個(gè)變量時(shí), 控制流可以一次有一次的分裂和重新合并, 并可以觀察該變量的每個(gè)點(diǎn)具有不同的類型

例如:




function example() {
    let x: string | number | boolean;
    
    // 節(jié)點(diǎn)一
    x = Math.random() < 0.5;
    console.log(x);
    // let x: boolean

    if (Math.random() < 0.5) {
        // 節(jié)點(diǎn)二
        x = "hello";
        console.log(x);
        // let x: string
        
    } else {
        // 節(jié)點(diǎn)三
        x = 100;
        console.log(x);
        // let x: number
    }
    
    // 節(jié)點(diǎn)四
    console.log(x)
    // let x: string | number
}

示例中聲明一個(gè)變量, 并聲明可分配類型為string | number | boolean聯(lián)合類型

在節(jié)點(diǎn)一的時(shí)候, 給x賦值一個(gè)結(jié)果為boolean類型的表達(dá)式, 此時(shí)x類型收窄為boolean

接下來(lái)進(jìn)入流程控制:

在條件為true時(shí),即節(jié)點(diǎn)二時(shí),x被賦值為string類型的值, 此時(shí)TypeScript 刪除原來(lái)收窄的類型,重新收窄為string類型

條件為false 時(shí), 即節(jié)點(diǎn)三時(shí), x被賦值為 number類型的值, 此時(shí)TypeScript重新收窄x變量的類型為number類型

當(dāng)結(jié)束流程控制時(shí), TypeScript進(jìn)行分析, 發(fā)現(xiàn)boolean類型是通不過(guò)流程控制的, 在流程控制結(jié)束后,條件為true, x為string類型,條件為false ,x為number類型

此時(shí)TypeScript在流程控制結(jié)束后, 將x變量的類型收窄為string | number, 刪除通不過(guò)的boolean類型


8. 類型謂詞,

到目前為止,我們已經(jīng)使用現(xiàn)有的 JavaScript 結(jié)構(gòu)來(lái)處理縮小范圍,但是有時(shí)您希望更直接地控制類型在整個(gè)代碼中的變化方式。

定義用戶類型包含, 需要定義一個(gè)返回類型為類型謂詞的函數(shù)

const isStudent = (student: Student | Doctor):student is Student => {
    return (student as Student).age !== undefined
}

這個(gè)函數(shù)的返回值寫法student is Student就是示例中的類型謂詞, 謂詞采用的形式parameterName is Type, 其中parameterName必須是當(dāng)前函數(shù)簽名中的參數(shù)名稱

任何時(shí)候, isStudent調(diào)用某個(gè)變量, 如果類型兼容, TypeScript將會(huì)將改變了縮小到特定的類型

示例

// 自定義類型包含,類型謂詞
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. 識(shí)別聯(lián)合

識(shí)別聯(lián)合類型: 就是通過(guò)聯(lián)合類型相同的屬性來(lái)區(qū)分聯(lián)合類型,

例如,假設(shè)我們嘗試去圓形和正方形形狀進(jìn)行編碼, 圓形記錄半徑, 正方形記錄邊長(zhǎng),我們將字段使用kind來(lái)判斷處理的形狀

// 定義接口
interface Shape{
    kind: 'circle' | 'square'
    radius?:number
    sideLength?: number
}

注意: 智力我們使用了字符串文字類型的聯(lián)合,circle和'square'用于區(qū)分圓形還是方形,

此時(shí)我們就可以編寫一個(gè)getArea函數(shù),根據(jù)它是圓形還是方形來(lái)應(yīng)用正確的邏輯,我們首先處理圓形

function getArea(shape:Shape){
    return Math.PI * shape.radius ** 2
}

此時(shí)radius屬性可能沒(méi)有定義, 就會(huì)導(dǎo)致 程序問(wèn)題, 此時(shí) 我們就會(huì)想到對(duì)kind屬性進(jìn)行適當(dāng)?shù)臋z查,

function getArea(shape:Shape){
    if(shape.kind === 'circle'){
        return Math.PI * shape.radius ** 2 
    }
}

此時(shí)又會(huì)帶來(lái)另一個(gè)問(wèn)題, radius的值 可能是undefined, 此時(shí)可以使用非空斷言(!)來(lái)告訴類型檢查器radius肯定存在

function getArea(shape:Shape){
    if(shape.kind === 'circle'){
        return Math.PI * shape.radius! ** 2
    }
}

但這感覺(jué)并不理想, 因?yàn)槲覀儾坏貌皇褂梅强諗嘌? 無(wú)論如何,我們都可能意外訪問(wèn)radius,sideLength中的任何一個(gè),(因?yàn)樵谧x取它們時(shí)可選屬性始終存在),

這種編碼的問(wèn)題是類型檢查器沒(méi)有任何方法可以根據(jù)屬性Shape知道是否存在, 我們需要將我們所知道的信息傳達(dá)給類型檢查器, 考慮這一點(diǎn),我們可以重新定義接口

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;
                      
  }
}

當(dāng)聯(lián)合中的每個(gè)類型都包含具有文字類型的公共屬性時(shí),TypeScript 認(rèn)為這是一個(gè)可區(qū)分的聯(lián)合,并且可以縮小聯(lián)合的成員范圍。

在這種情況下,kind是那個(gè)共同屬性(這被認(rèn)為是 的判別屬性Shape)。檢查該kind屬性是否被"circle"刪除了所有Shape沒(méi)有typekind屬性的類型"circle"。那縮小shape到類型Circle。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容