TypeScript真香系列-類型推論和類型兼容性

前言

TypeScript真香系列的內容將參考中文文檔,可是文中的例子基本不會和文檔中的例子重複,對於一些地方也會深刻研究。另外,文中一些例子的結果都是在代碼沒有錯誤後編譯爲JavaScript獲得的。若是想實際看看TypeScript編譯爲JavaScript的代碼,能夠訪問TypeScript的在線編譯地址,動手操做,印象更加深入。javascript

類型推論

基礎

TypeScript中的類型推論,就是當咱們沒有明確指定變量的類型時,TypeScript能夠自動的推斷出變量的數據類型。java

let a = 3;
a = 4;
a = "s";  //錯誤,"s"和number類型不匹配

從上面的例子能夠看出,當咱們定義了一個變量a,而後進行賦值,TypeScript就自動給咱們推斷出變量a的類型。當咱們再給變量a賦值爲字符串的時候,就會出現代碼中的錯誤提示。這樣的寫法在JavaScript中是能夠的,可是在TypeScript中給咱們進行了限制。git

let a = {
    p: "",
    c: 0
};
a.p = "火影";
a.p = 1; //錯誤,1和string類型不匹配

最佳通用類型

上面的例子很簡單,可是當咱們定義的變量爲數組這樣比較複雜的類型的時候,TypeScript就會根據其中的成員來推斷出最合適的通用類型:github

let a = [1, 2, null];
a=["s"];  //錯誤,類型"string"和"number | null"不匹配

上下文類型

上面的例子都是通從右到左判出的類型,TypeScript類型推論也可能按相反的方向來推斷,這被叫作「按上下文歸類」,按上下文歸類會發生在表達式的類型與所處的位置相關時。下面的例子是在函數這一節的:typescript

function sum(a: number, b: number){
    return a + b;
}

咱們沒有指定返回值的類型,可是TypeScript自動從上到下推斷出返回值的類型爲c#

number。
let man = {
    a: 1,
    b: "james",
    play: (s: string) => {
        return s
    }
}

man.play = function (s){ 
   return s + "s"
}

類型兼容性

基礎

TypeScript中的類型兼容性能夠用於肯定一個類型是否能夠賦值給其餘類型。這裏要了解兩個概念:數組

結構類型:一種只使用其成員來描述類型的方式;
名義類型:明確的指出或聲明其類型,如c#,java。

TypeScript的類型兼容性就是基於結構子類型的。下面的例子:函數

interface IName {
    name: string;
}

class Man {
    name: string;
    constructor() {
        this.name = "鳴人";
    }
}

let p: IName;
p = new Man();
p.name;

上面的代碼在TypeScript不會出錯,可是在java等語言中就會報錯,由於Man類沒有明確的說明實現了IName 接口。可能有人會感受上面的例子體現不了什麼,那咱們接下來看下面的不兼容的例子:this

let man: string = "佐助";
let age: number = 20;

man = age;  // 錯誤,類型number和類型string不匹配
age = man;  // 錯誤,類型string和類型number不匹配

再看個兼容的例子:rest

let man: any = "佐助";
let age: any = 123
man = age;  //123

結構化

TypeScript結構化類型系統的基本規則是,若是x要兼容y,那麼y至少具備與x相同的屬性。以下面的例子:

interface IName { 
    name: string;
}
let x: IName;
let y = {name: "鳴人", age: 123, hero: true};
x = y;  //{name: "鳴人", age: 123, hero: true}

這裏編譯器檢查了x中的每個屬性,看是否在y中也能找到對應的屬性。而上面的 y 符合了 x 兼容的要求,即x兼容y。

interface IName { 
    name: string;
    age: number
}
let x: IName;

let z = { name: "佐助", cool: true };
x = z; // 錯誤

這裏編譯器在檢查的時候,發現 z 中少了 x 中的"age"這個屬性,因此 x 和 z 是不兼容的。

比較函數

參數不一樣

上面的例子都是一些原始類型或者對象之間的比較,如今咱們看看函數之間是怎麼比較的:

let x = (a: number) => 0;
let y = (b: number, c: string) => 0;

y = x; 
x = y; //錯誤

要看x可否賦值給y,先看x和y的參數列表。x的每一個參數都必須在y裏面找到對應類型的參數,只要參數類型相對應,參數名字無所謂。上面例子中x的參數都能在y中找到對應的參數,因此容許賦值,可是反過來,y就不能給x賦值。

函數參數的雙向協變

雙向協變包含協變和逆變。協變是指子類型兼容父類型,而逆變正好相反。

let man = (arg: string | number) : void => {};
let player = (arg: string) : void => {};

man = player;
player = man;

可選和rest參數

關於可選參數和rest參數的兼容,能夠看下面的例子:

let man = (x: number, y: number) => {};
let work = (x?: number, y?: number) => {};
let play = (...args: number[]) => {};

man = work = play;
play = work = man;

函數重載

關於重載,咱們先看看java中的定義:
在同一個類中,容許存在一個以上的同名函數,只要他們的參數個數或者參數類型不一樣便可。與返回值類型無關,只看參數列表(參數的個數、參數的類型、參數的順序)

在TypeScript中的函數重載和java中的不一樣,TypeScript中的函數重載僅僅是參數類型重載:

function sum(a: number, b: number): number;
function sum(a: string, b: string): string;

function sum(a: any, b: any) {
    let result = null;
    if (typeof a === "string" && typeof b === "string") {
        result = <string>a + "和" + <string>b + "是好基友";
    } else if (typeof a === "number" && typeof b === "number") {
        result = <number>a + <number>b
    }
    return result;
}
sum("鳴人", "佐助");
sum(1, 1);

對於有重載的函數,源函數的每一個重載都要在目標函數上找到對應的函數簽名,下面這種方式就是錯誤的:

function sum(a: number, b: number): number; // 錯誤,重載簽名與其實現簽名不兼容
function sum(a: string, b: string): string{
    return a + b;
};

下面這個例子在TypeScript也不能進行重載:

function sum(a: number, b: number): number{ //錯誤,函數重複實現
    return a + b;
};
function sum(a: any, b: any): any{ //錯誤,函數重複實現
    return a + b;
};

返回值不一樣

而後咱們看看返回值類型怎麼比較的,源函數的返回類型必須是目標函數返回值的子類型:

interface IMan {
  x: string;
  y: number;
}
interface IPlayer {
  x: string;
  y: number;
  z: number;
}
 
let man = (): IMan => ({ x: "鳴人", y: 0 });
let player = (): IPlayer => ({ x: "佐助", y: 0, z: 0 });
 
man = player;
player = man; //錯誤

從上面能夠看出,player是man的子類型,因此man兼容player。下面這個例子也體現了這一點:

interface IMan {
  x: string;
  y: number;
}
interface IPlayer {
  a: string;
  b: number;
  c: number;
}
 
let man = (): IMan => ({ x: "鳴人", y: 0 });
let player = (): IPlayer => ({ a: "佐助", b: 0, c: 0 });
 
man = player; //錯誤
player = man; //錯誤

枚舉

枚舉類型和數字類型相互兼容:

enum Man {
    name,
    age,
}

let num = 1;
let num2 = 2;
let enumNum: Man.name = num; 
num2 = Man.name;

不一樣枚舉之間是不兼容的:

enum Man {
    name,
    age,
}

enum Player { 
    name,
    age,
}
let man: Man.name = Player.name; //錯誤,類型Player.name不能分配給類型 Man.name
let player: Player.age = Man.age;  //錯誤,類型 Man.name不能分配給類型 Player.name

類的基本比較

在TypeScript中,只有實例成員和方法會被比較,靜態成員和構造函數不會被比較。

class Man {
    name: string;
    constructor(arg: string,) {
        this.name = arg;
    }
    showName() {
        return this.name;
    }
}

class Player {
    static age: number;
    name: string;
    constructor(arg: string, hero: boolean) {
        this.name = arg;
    }
    showName() {
        return this.name;
    }
}

let man = new Man("佐助");
let player = new Player("鳴人", true);

man = player;
player = man;

從上面的例子能夠看出,雖然兩個類有着不一樣的構造函數和靜態成員,可是他們有相同的實例成員和方法,因此他們之間是兼容的。

類的私有成員和受保護成員

類的私有成員和受保護成員的兼容性的比較規則是同樣的。比較兩個類的時候要分兩種狀況來看,當兩個類是父子類,父類中有私有成員的時候,兩個類是兼容的;當兩個類是同級的類的時候,並且同級類中包含私有或受保護成員時,就不兼容了。看看下面的兩個例子:
父子類:

class Man {
    private name: string;
    
    constructor(arg: string) {
        this.name = arg;
    }
    
}

class Player extends Man {
    
    constructor(arg: string) {
        super(arg);
    }
    
}

let man = new Man("鳴人");
let player = new Player("佐助");
//Man類和Player類是父子類,因此兩個類是兼容的
man = player;
player = man;

同級類:

class Man {
    private name: string;
    constructor(arg: string) {
        this.name = arg;
    }
    
}

class Player {
    private name: string;
    constructor(arg: string) {
       this.name = arg;
    }
    
}

let man = new Man("鳴人");
let player = new Player("佐助");

man = player; // 錯誤,類型Player不能分配給類型Man,類型具備私有屬性name的單獨聲明
player = man; // 錯誤,類型Man不能分配給類型Player,類型具備私有屬性name的單獨聲明

泛型

TypeScript泛型的兼容性分兩種狀況,一種是類型參數沒有被成員使用;另外一種是類型參數被成員使用。
咱們先看當類型參數沒有被成員使用時:

interface IMan<T>{

}

let man1: IMan<number>;
let man2: IMan<string>;
man1 = man2;
man2 = man1;

當類型參數被成員使用時:

interface IMan<T>{
    name: T;
}

let man1: IMan<number>;
let man2: IMan<string>;
man1 = man2;  //錯誤,IMan<string>不能分配給IMan<number>
man2 = man1;  //錯誤,IMan<number>不能分配給IMan<string>


interface IMan<T>{
    name: T;
}

let man1: IMan<number>;
let man2: IMan<number>;
man1 = man2;  
man2 = man1;

在TypeScript的泛型中,若是類型參數沒有被成員使用時,對兼容性沒有影響;若是參數被成員使用,則會影響兼容性。

參考

https://github.com/zhongsp/Ty...
https://github.com/jkchao/typ...

最後

文中有些地方可能會加入一些本身的理解,如有不許確或錯誤的地方,歡迎指出~

相關文章
相關標籤/搜索