1. 程式人生 > 實用技巧 >細說Typescript型別檢查機制

細說Typescript型別檢查機制

上一篇文章我介紹了Typescript的基礎知識,回顧一下,有基礎資料型別、陣列、函式、類、介面、泛型等,本節內容將述說一下Typescript為方便我們開發提供了一些型別檢查機制。

型別檢查機制

型別檢查機制: Typescript編譯器在做型別檢查時,所秉承的一些原則,以及表現出的一些行為。

作用:輔助開發,提高開發效率。主要有以下三點:

  1. 型別推斷
  2. 型別相容性
  3. 型別保護

1、型別推斷

不需要指定變數的型別(函式的返回值型別), Typescript可以根據某些規則自動地為其推斷出一個型別。

  • 基礎型別推斷。是ts中比較常見的型別推斷。

// 沒有設定型別時,TS會根據賦值的型別推斷型別
let a1;     // 等價於 let a1: any let a
= 1;   // 等價於 let a: number let b = [1];  // 等價於 let b: number[] let c = [1, null];      // 等價於 let c: number[] let c1 = (x = 1) => {}   // 等價於 let c1: (x?: number) => void let d = (x = 1) => x + 1; // 等價於 let d: (x?: number) => number

  • 最佳通用型別推斷
// 斷言好處:改造舊程式碼會很有效,但避免濫用,需對上下文環境有充足的預判,否則會帶來安全隱患
interface Foo { bar: number } // let foo = {}; // 報錯 // let foo = {} as Foo; // 注意:不可濫用斷言,若去掉bar屬性,foo物件就沒有按照介面的嚴格約定返回,就會遺漏 // foo.bar = 1; let foo: Foo = { // 最佳通用型別推斷 bar: 1 };
  • 總結:TS的型別推斷可以為我們提供一些重要的輔助資訊,但是需要合理使用

2、型別相容性

當一個型別 y 可以被賦值給另一個型別 x 時,我們就可以說型別 x 相容型別 y x 相容 y : x (目標型別)= y(源型別);

簡單理解:

介面之間相容:成員少的相容成員多的

函式之間相容:引數多的相容引數少的

2.1 型別相容

// x 相容 y : x (目標型別) =  y(源型別)
let s: string = 'a'
s = null; // 報錯

上述的報錯,在tsconfig.json 設定strictNullChecks:false即可

2.2 介面相容

介面之間相容:成員少的會相容成員多的

interface XX {
    a: any;
    b: any;
}
interface YY {
    a: any;
    b: any;
    c: any;
}
let xx: XX = {a:1, b:2}
let yy: YY = {a:1, b:2, c:3}

xx = yy;
// yy = xx; // 報錯。注意:源型別必須要具備目標型別的必要屬性。xx可以相容yy型別,反之不行

2.3 函式相容

判斷兩個函式是否相容,一般會發生在相互賦值的場景下。

函式之間相容:引數多的相容引數少的

type Handler = (a: number, b: number) => void; // Handler 為目標型別,傳入的引數為源型別
function hocf(handler: Handler){
    return handler
}
// 目標函式要相容源函式需要同時滿足以下三點:
// 1) 引數個數要求: 目標函式的個數要多於源函式引數的個數
    // 固定引數個數
    let handler1 = (a: number) => {};
    hocf(handler1);
    // let handler2 = (a: number, b: number, c: number) => {};
    // hocf(handler2); // 報錯。目標函式的個數要多於源函式引數的個數
    
    // 不固定引數:可選引數和剩餘引數: 需堅持三原則
    let l = (p1: number, p2: number) => {}
    let m = (p1?: number, p2?: number) => {}
    let n = (...args: number[]) => {}
    // - 1.固定引數可以相容可選引數和剩餘引數
    l = m;
    l = n;
    // - 2.可選引數不相容固定引數和剩餘引數,但設定 tsconfig strictFunctionTypes:false 屬性,可預防報錯
    m = n // 報錯
    m = n // 報錯
    // - 3.剩餘引數可以相容固定引數和可選引數
    n = l
    n = m
    
// 2) 引數型別要求
    // 基礎型別介面
    let handler3 = (a: string) => {};
    // hocf(handler3); // 報錯。型別不相容

    // 物件型別介面:成員多的相容成員少的,與介面成員相容相反,可以看成引數,引數多的相容引數少的
    interface Point3D {
        x: number;
        y: number;
        z: number;
    }
    interface Point2D {
        x: number;
        y: number;
    }
    // 引數個數相同,引數型別相同Object
    let p3d = (point: Point3D) => {}
    let p2d = (point: Point2D) => {}
    p3d = p2d
    p2d = p3d // 報錯。設定 tsconfig strictFunctionTypes:false 屬性,可預防報錯
    // 函式引數之間可以相互賦值的情況被稱為 引數雙向協變。作用:可以把精確型別 賦值給 不精確型別,這樣就不需要把不精確的型別斷言成精確型別

// 3)  返回值型別。ts要求我們目標函式的返回值型別必須要和源函式返回值型別一致或者是其子型別
    // 成員少的相容成員多的 
    let o = () => ({name: "Alice"});
    let p = () => ({name: "Alice", location: 'Beijing'});
    o = p;
    // p = o; // 報錯

2.4 函式過載相容

// 在過載中,目標函式的引數要多於源函式的引數
function overload(a: number, b: number): number // 列表中的函式:目標函式
function overload(a: string, b: string): string // 列表中的函式:目標函式
function overload(a: any, b: any): any {} // 具體實現:源函式
// function overload(a: any, b: any, c: any): any {} // 報錯。在過載中,目標函式的引數要多於源函式的引數
// function overload(a: any, b: any) {} // 報錯。返回值引數型別不相容

2.5 列舉相容

enum Fruit { Apple, Banana }
enum Color { Red, Yellow }
// 列舉型別和數字型別是完全可以互相相容和賦值的
let fruit: Fruit.Apple = 3; 
let no: number = Fruit.Apple;
// 列舉型別之間是不相容的
// let color: Color.Red = Fruit.Apple; // 報錯

2.6 類相容

類相容和介面相容相似,都是隻比較結構;

1. 建構函式constructor、和靜態成員static是不作為比較的;

2. 如果兩個類具有相同的例項成員,這兩個類之間也是相容的;

3. 類中有私有成員 private,兩個類是不相容的,只有父類和子類是相容的;

class A {
    constructor(p: number, q: number){}
    id: number = 1
    // private name: string = ''// 3. 報錯
}
class B {
    static s = 1;
    constructor(p: number){}
    id: number = 2
    // private name: string = '' // 3. 報錯
}

let aa = new A(1,2)
let bb = new B(1)
// 2. 都具有id屬性,所以相互相容
aa = bb; 
bb = aa;
// 3. 父類和子類是相容的
class An extends A { }
let an = new An(1,2)
aa = an;
an = aa;

2.7 泛型相容

// - 不會報錯
interface Empty<T>{  }
let obj1: Empty<number> = {  }
let obj2: Empty<number> = {  }
obj1 = obj2;
// - 會報錯 只有介面引數T被介面使用的時候,才會影響泛型的相容性
// interface Empty<T>{
//     value: T
// }
// let obj1: Empty<number> = {}
// let obj2: Empty<number> = {}
// obj1 = obj2;

// 2. 泛型函式
let loog1 = <T>(x: T): T => {
    console.log('x')
    return x
}
let loog2 = <U>(y: U): U => {
    console.log('y')
    return y
}
loog1 = loog2; // 兩個泛型函式的定義相同,並沒有指定型別引數,泛型函式也是相容的

2.8 總結

1、考慮型別相容是因為ts允許我們把一些型別不同的變數相互賦值,雖然在某種程度上講,會產生不可靠的行為,但這個特性卻增加了語言的靈活性

2、型別相容的使用場景:廣泛存在介面、函式、類中 演示

3、型別保護

定義: Typescript能夠在特定的區塊(型別保護區塊)中保證變數屬於某種確定的型別。 可以在此區塊中放心地引用此型別的屬性,或者呼叫此型別的方法。

換句話說,型別保護可以保證一個字串是一個字串,儘管它的值也可以是一個數值。

enum Type { Strong, Week }
class Java {
    helloJava(){
        console.log('Hello Java')
    }
    java: any
}
class JavaScript {
    helloJavaScript(){
        console.log('Hello JavaScript')
    }
    javascript: any
}
function getLanguage(type: Type, x: string|number){
    let lang = type === Type.Strong ? new Java() : new JavaScript();
    // 用型別斷言解決返回值不確定的情況,可讀性差
    if((lang as Java).helloJava){
        (lang as Java).helloJava()
    } else {
        (lang as JavaScript).helloJavaScript()
    }
    // console.log(lang, 'lang')
    return lang;
}
getLanguage(Type.Strong, '11')

上述的解決方案顯然不是最佳方案,有以下四種方案可以解決型別保護:

3.1 方案1:instanceof

function getLanguage(type: Type, x: string|number){
    let lang = type === Type.Strong ? new Java() : new JavaScript();
   if(lang instanceof Java){
        lang.helloJava()
    } else {
        lang.helloJavaScript()
    }
    // console.log(lang, 'lang')
    return lang;
}

3.2 方案2:in

function getLanguage(type: Type, x: string|number){
    let lang = type === Type.Strong ? new Java() : new JavaScript();
  if('java' in lang){
        lang.helloJava()
    } else {
        lang.helloJavaScript()
    }
    // console.log(lang, 'lang')
    return lang;
}

3.3 方案3:typeof

function getLanguage(type: Type, x: string|number){
    let lang = type === Type.Strong ? new Java() : new JavaScript();
  // x也是聯合型別,typeof型別保護,可以判斷出基本型別。
    if(typeof x === 'string'){
        x.length
    } else {
        x.toFixed
    }
    // console.log(lang, 'lang')
    return lang;
}

3.4 方案4:型別保護函式

function getLanguage(type: Type, x: string|number){
    let lang = type === Type.Strong ? new Java() : new JavaScript();
  if(isJava(lang)){
        lang.helloJava()
    }else{
        lang.helloJavaScript()
    }

    // console.log(lang, 'lang')
    return lang;
}

定義一個型別保護函式:

// 型別保護函式,可以傳遞任何值給 isJava 函式,用來判斷它是不是 Java。isJava 函式與普通函式的最大區別是,該函式的返回型別是 lang is Java,這就是 "型別謂詞"。
function isJava(lang: Java | JavaScript): lang is Java{
// 型別謂詞可參考:https://segmentfault.com/a/1190000022883470
return (lang as Java).helloJava !== undefined // 返回結果是 true,那麼當前判斷的 lang 變數值的型別是 Java 型別。
}

型別保護作用:

1. 可以提前對型別做預判

2. 返回型別謂詞,如 lang is Java;

3. 包含可以準確確定給定變數型別的邏輯語句,如 (lang as Java).helloJava !== undefined。

3.5 總結

不同的判斷方法有不同的使用場景:

typeof:判斷一個變數的型別(多用於基本型別);

instanceof:判斷一個例項是否屬於某個類;

in:判斷一個屬性是否屬於某個物件;

型別保護函式:某些判斷可能不是一條語句能夠搞定的,需要更多複雜的邏輯,適合封裝到一個函式內。

結合ts型別檢查機制再配合vscode 自動補全和自動修復功能,能夠極大的提高我們的開發效率!

4、結尾

截止本節,Typescript的基礎知識已經介紹差不多了,後期如有時間會繼續更新TS的進階高階部分,感謝您的閱讀~