TypeScript學習——類型兼容性、高級類型

類型兼容性

TypeScript里的類型兼容性是基于結構子類型的。 結構類型是一種只使用其成員來描述類型的方式。 它正好與名義(nominal)類型形成對比。在基于名義類型的類型系統中,數據類型的兼容性或等價性是通過明確的聲明和/或類型的名稱來決定的。這與結構性類型系統不同,它是基于類型的組成結構,且不要求明確地聲明。TypeScript的結構性子類型是根據JavaScript代碼的典型寫法來設計的。 因為JavaScript里廣泛地使用匿名對象,例如函數表達式和對象字面量,所以使用結構類型系統來描述這些類型比使用名義類型系統更好。

TypeScript結構化類型系統的基本規則是,如果x要兼容y,那么y至少具有與x相同的屬性:

interface Named {
    name: string;
}

let x: Named;
// 檢查y是否能賦值給x,編譯器檢查x中的每個屬性,看是否能在y中也找到對應屬性。
let y = { name: 'Alice', location: 'Seattle' };
x = y;
比較兩個函數
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

要查看x是否能賦值給y,首先看它們的參數列表。 x的每個參數必須能在y里找到對應類型的參數。 注意的是參數的名字相同與否無所謂,只看它們的類型。 這里,x的每個參數在y中都能找到對應的參數,所以允許賦值。

第二個賦值錯誤,因為y有個必需的第二個參數,但是x并沒有,所以不允許賦值。

注意:返回值不同的函數情況又不一樣了:

let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error, because x() lacks a location property

類型系統強制源函數的返回值類型必須是目標函數返回值類型的子類型。

枚舉

枚舉類型與數字類型兼容,并且數字類型與枚舉類型兼容。不同枚舉類型之間是不兼容的:

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let status = Status.Ready;
status = Color.Green;  // Error

類與對象字面量和接口差不多,但有一點不同:類有靜態部分和實例部分的類型。 比較兩個類類型的對象時,只有實例的成員會被比較。 靜態成員和構造函數不在比較的范圍內。

class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}

class Size {
    feet: number;
    constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = s;  // OK
s = a;  // OK
泛型

因為TypeScript是結構性的類型系統,類型參數只影響使用其做為類型一部分的結果類型。

高級類型

交叉類型(Intersection Types)

交叉類型是將多個類型合并為一個類型。 這讓我們可以把現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

class Person {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
聯合類型(Union Types)

聯合類型表示一個值可以是幾種類型之一。 用豎線( |)分隔每個類型,所以 number | string | boolean表示一個值可以是 number, string,或 boolean。

如果一個值是聯合類型,只能訪問此聯合類型的所有類型里共有的成員。

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors
交叉類型與聯合類型的區別

為了更好的理解,有例子如下:

interface Foo {
  foo: string;
  name: string;
}
 
interface Bar {
  bar: string;
  name: string;
}
 
const sayHello = (obj: Foo | Bar) => { /* ... */ };
 
sayHello({ foo: "foo", name: "lolo" });
sayHello({ bar: "bar", name: "growth" });

const sayHello = (obj: Foo & Bar) => { /* ... */ };
 
sayHello({ foo: "foo", bar: "bar", name: "kakuqo" });

聯合類型 A | B 表示一個集合,該集合是與類型A關聯的一組值和與類型 B 關聯的一組值的并集。交叉類型 A & B 表示一個集合,該集合是與類型 A 關聯的一組值和與類型 B 關聯的一組值的交集。
因此,Foo | Bar 表示有 foo 和 name 屬性的對象集和有 bar 和 name 屬性的對象集的并集。屬于這類集合的對象都含有 name 屬性。有些有 foo 屬性,有些有 bar 屬性。
而 Foo & Bar 表示具有 foo 和 name 屬性的對象集和具有 bar 和 name 屬性的對象集的交集。換句話說,集合包含了屬于由 Foo 和 Bar 表示的集合的對象。只有具有這三個屬性(foo、bar 和 name)的對象才屬于交集。

類型保護與區分類型(Type Guards and Differentiating Types)
  • 用戶自定義的類型保護
    要定義一個類型保護,需要定義一個函數,它的返回值是一個 類型謂詞
function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

pet is Fish就是類型謂詞。 謂詞為 parameterName is Type這種形式, parameterName必須是來自于當前函數簽名里的一個參數名。

  • typeof類型保護
    不必將 typeof x === "xxx"抽象成一個函數,因為TypeScript可以將它識別為一個類型保護:
function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

這些typeof類型保護只有兩種形式能被識別: typeof v === "typename"typeof v !== "typename", "typename"必須是 "number", "string", "boolean"或 "symbol"。

  • instanceof類型保護
    instanceof類型保護是通過構造函數來細化類型的一種方式:
interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// 類型為SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // 類型細化為'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 類型細化為'StringPadder'
}

instanceof的右側要求是一個構造函數,TypeScript將細化為:

  1. 此構造函數的 prototype屬性的類型,如果它的類型不為 any的話
  2. 構造簽名所返回的類型的聯合
類型別名

類型別名會給一個類型起個新名字。 類型別名有時和接口很像,但是可以作用于原始值,聯合類型,元組以及其它任何需要手寫的類型:

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

起別名不會新建一個類型 - 它創建了一個新 名字來引用那個類型。 給原始類型起別名通常沒什么用,盡管可以做為文檔的一種形式使用。
同接口一樣,類型別名也可以是泛型 - 可以添加類型參數并且在別名聲明的右側傳入:

type Container<T> = { value: T };

也可以使用類型別名來在屬性里引用自己:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

接口與類型別名的區別
其一,接口創建了一個新的名字,可以在其它任何地方使用。 類型別名并不創建新名字—比如,錯誤信息就不會使用別名。 在下面的示例代碼里,在編譯器中將鼠標懸停在 interfaced上,顯示它返回的是 Interface,但懸停在 aliased上時,顯示的卻是對象字面量類型:

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

另一個重要區別是類型別名不能被 extendsimplements(自己也不能 extendsimplements其它類型)。 因為 軟件中的對象應該對于擴展是開放的,但是對于修改是封閉的,應該盡量去使用接口代替類型別名。
另一方面,如果無法通過接口來描述一個類型并且需要使用聯合類型或元組類型,這時通常會使用類型別名。

字符串字面量類型

字符串字面量類型允許指定字符串必須的固定值。 在實際應用中,字符串字面量類型可以與聯合類型,類型保護和類型別名很好的配合。 通過結合使用這些特性,可以實現類似枚舉類型的字符串:

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
多態的 this類型

多態的 this類型表示的是某個包含類或接口的 子類型。 這被稱做 F-bounded多態性。 它能很容易的表現連貫接口間的繼承,比如。 在計算器的例子里,在每個操作之后都返回 this類型:

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();
索引類型(Index types)

在TypeScript里使用此函數,通過 索引類型查詢和 索引訪問操作符:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]

編譯器會檢查 name是否真的是 Person的一個屬性。 本例還引入了幾個新的類型操作符。 首先是 keyof T, 索引類型查詢操作符。 對于任何類型 T, keyof T的結果為 T上已知的公共屬性名的聯合。 例如:

let personProps: keyof Person; // 'name' | 'age'

keyof Person是完全可以與 'name' | 'age'互相替換的。 不同的是如果添加了其它的屬性到 Person,例如 address: string,那么 keyof Person會自動變為 'name' | 'age' | 'address'。

第二個操作符是 T[K], 索引訪問操作符。 在這里,類型語法反映了表達式語法。 這意味著 person['name']具有類型 Person['name'] — 在例子里則為 string類型。 然而,就像索引類型查詢一樣,可以在普通的上下文里使用 T[K],這正是它的強大所在。 只要確保類型變量 K extends keyof T就可以了。 例如下面 getProperty函數的例子:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

keyof和 T[K]與字符串索引簽名進行交互。 如果你有一個帶有字符串索引簽名的類型,那么 keyof T會是 string。 并且 T[string]為索引簽名的類型:

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number
映射類型

TypeScript提供了從舊類型中創建新類型的一種方式 — 映射類型。 在映射類型里,新類型以相同的形式去轉換舊類型里每個屬性。 例如,令每個屬性成為 readonly類型或可選的。 下面是一些例子:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容