TS 3.1 - 高級類型(交叉類型、聯合類型、類型保護、null和undefined、別名、可辨識聯合、this類型、keyof索引及索引訪問類型、映射、預定義映射)


總結:

  • extends 先進行了類型判斷,需要聯合類型中每一項都滿足條件時才進行分別循環判斷

原文地址 www.tslang.cn

交叉類型(Intersection Types)

交叉類型是將多個類型合並為一個類型。 這讓我們可以把現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。 例如, Person & Serializable & Loggable同時是 Person Serializable Loggable。 就是說這個類型的對象同時擁有了這三種類型的成員。

注釋:函數的重載可以視為交叉類型

  type A = (a: string) => string
  type B = (b:number) => number
  const abc: A & B = a => a;

聯合類型(Union Types)

在傳統的面向對象語言里,我們可能會將這兩種類型抽象成有層級的類型。 這么做顯然是非常清晰的,但同時也存在了過度設計。 padLeft原始版本的好處之一是允許我們傳入原始類型。 這樣做的話使用起來既簡單又方便。 如果我們就是想使用已經存在的函數的話,這種新的方式就不適用了。

代替 any, 我們可以使用 _聯合類型_做為 padding的參數:

function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // errors during compilation

類型保護與區分類型(Type Guards and Differentiating Types)

使用類型斷言:

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

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

用戶自定義的類型保護

類型保護就是一些表達式,它們會在運行時檢查以確保在某個作用域里的類型。 要定義一個類型保護,我們只要簡單地定義一個函數,它的返回值是一個 類型謂詞

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

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

每當使用一些變量調用 isFish時,TypeScript 會將變量縮減為那個具體的類型,只要這個類型與變量的原始類型是兼容的。

// 'swim' 和 'fly' 調用都沒有問題了

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

typeof類型保護

我們不必將 typeof x === "number"抽象成一個函數,因為 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"。 但是 TypeScript 並不會阻止你與其它字符串比較,語言不會把那些表達式識別為類型保護。

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. 構造簽名所返回的類型的聯合

可以為null的類型

TypeScript 具有兩種特殊的類型, nullundefined,它們分別具有值 null 和 undefined. 我們在 [基礎類型](./Basic Types.md) 一節里已經做過簡要說明。 默認情況下,類型檢查器認為 nullundefined可以賦值給任何類型。 nullundefined是所有其它類型的一個有效值。 這也意味着,你阻止不了將它們賦值給其它類型,就算是你想要阻止這種情況也不行。 null的發明者,Tony Hoare,稱它為 價值億萬美金的錯誤

--strictNullChecks標記可以解決此錯誤:當你聲明一個變量時,它不會自動地包含 nullundefined。 你可以使用聯合類型明確的包含它們:

可選參數和可選屬性

使用了 --strictNullChecks,可選參數會被自動地加上 | undefined:

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

可選屬性也會有同樣的處理:

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

類型保護和類型斷言

由於可以為 null 的類型是通過聯合類型實現,那么你需要使用類型保護來去除 null。 幸運地是這與在 JavaScript 里寫的代碼一致:

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

如果編譯器不能夠去除 nullundefined,你可以使用類型斷言手動去除。 語法是添加 !后綴: identifier!identifier的類型里去除了 nullundefined

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

本例使用了嵌套函數,因為編譯器無法去除嵌套函數的 null(除非是立即調用的函數表達式)。 因為它無法跟蹤所有對嵌套函數的調用,尤其是你將內層函數做為外層函數的返回值。 如果無法知道函數在哪里被調用,就無法知道調用時 name的類型。

類型別名

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

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

與交叉類型一起使用,我們可以創建出一些十分稀奇古怪的類型。

注釋:接口也可以實現相同的功能

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

然而,類型別名不能出現在聲明右側的任何地方。

注釋:不能在右側的說法是有問題的,在 4.1.2 版本中以下示例不會報錯。

type Yikes = Array<Yikes>; // error

接口 vs. 類型別名

像我們提到的,類型別名可以像接口一樣;然而,仍有一些細微差別。

其一,接口創建了一個新的名字,可以在其它任何地方使用。 類型別名並不創建新名字—比如,錯誤信息就不會使用別名。 在下面的示例代碼里,在編譯器中將鼠標懸停在 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其它類型)。 因為 軟件中的對象應該對於擴展是開放的,但是對於修改是封閉的,你應該盡量去使用接口代替類型別名。

另一方面,如果你無法通過接口來描述一個類型並且需要使用聯合類型或元組類型,這時通常會使用類型別名。

注釋:開閉原則:當軟件需要變化時,盡量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現變化。

枚舉成員類型

如我們在 枚舉一節里提到的,當每個枚舉成員都是用字面量初始化的時候枚舉成員是具有類型的。

在我們談及 “單例類型” 的時候,多數是指枚舉成員類型和數字 / 字符串字面量類型,盡管大多數用戶會互換使用 “單例類型” 和“字面量類型”。

可辨識聯合(Discriminated Unions)

你可以合並單例類型,聯合類型,類型保護和類型別名來創建一個叫做 _可辨識聯合_的高級模式,它也稱做 _標簽聯合_或 代數數據類型。 可辨識聯合在函數式編程很有用處。 一些語言會自動地為你辨識聯合;而 TypeScript 則基於已有的 JavaScript 模式。 它具有 3 個要素:

  1. 具有普通的單例類型屬性— 可辨識的特征
  2. 一個類型別名包含了那些類型的聯合— 聯合
  3. 此屬性上的類型保護。
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

首先我們聲明了將要聯合的接口。 每個接口都有 kind屬性但有不同的字符串字面量類型。 kind屬性稱做 _可辨識的特征_或 標簽。 其它的屬性則特定於各個接口。 注意,目前各個接口間是沒有聯系的。 下面我們把它們聯合到一起:

type Shape = Square | Rectangle | Circle;

現在我們使用可辨識聯合:

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

完整性檢查

當沒有涵蓋所有可辨識聯合的變化時,我們想讓編譯器可以通知我們。 比如,如果我們添加了 TriangleShape,我們同時還需要更新 area:

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn't handle case "triangle"
}

有兩種方式可以實現。 首先是啟用 --strictNullChecks並且指定一個返回值類型:

function area(s: Shape): number { // error: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

因為 switch沒有包涵所有情況,所以 TypeScript 認為這個函數有時候會返回 undefined。 如果你明確地指定了返回值類型為 number,那么你會看到一個錯誤,因為實際上返回值的類型為 number | undefined。 然而,這種方法存在些微妙之處且 --strictNullChecks對舊代碼支持不好。

第二種方法使用 never類型,編譯器用它來進行完整性檢查:

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    }
}

這里, assertNever檢查 s是否為 never類型—即為除去所有可能情況后剩下的類型。 如果你忘記了某個 case,那么 s將具有一個真實的類型並且你會得到一個錯誤。 這種方式需要你定義一個額外的函數,但是在你忘記某個 case 的時候也更加明顯。

多態的 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();

由於這個類使用了 this類型,你可以繼承它,新的類可以直接使用之前的方法,不需要做任何的改變。

注釋:繼承來的 this 類型會指向當前類型,而不是繼承的類型

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .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索引類型查詢操作符。 對於任何類型 Tkeyof T的結果為 T上已知的公共屬性名的聯合。 例如:

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

keyof Person是完全可以與 'name' | 'age'互相替換的。 不同的是如果你添加了其它的屬性到 Person,例如 address: string,那么 keyof Person會自動變為 'name' | 'age' | 'address'。 你可以在像 pluck函數這類上下文里使用 keyof,因為在使用之前你並不清楚可能出現的屬性名。 但編譯器會檢查你是否傳入了正確的屬性名給 pluck

pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'

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

映射類型

注釋:映射使用了別名、泛型和 in 這個關鍵字

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

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

下面來看看最簡單的映射類型和它的組成部分:

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

它的語法與索引簽名的語法類型,內部使用了 for .. in。 具有三個部分:

  1. 類型變量 K,它會依次綁定到每個屬性。
  2. 字符串字面量聯合的 Keys,它包含了要迭代的屬性名的集合。
  3. 屬性的結果類型。

但它更有用的地方是可以有一些通用版本。

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

注釋:同態應該是指轉化后的對象包含的屬性是來源於轉換前的對象

在這些例子里,屬性列表是 keyof T且結果類型是 T[P]的變體。 這是使用通用映射類型的一個好模版。 因為這類轉換是 同態的,映射只作用於 T的屬性而沒有其它的。 編譯器知道在添加任何新屬性之前可以拷貝所有存在的屬性修飾符。 例如,假設 Person.name是只讀的,那么 Partial<Person>.name也將是只讀的且為可選的。

下面是另一個例子, T[P]被包裝在 Proxy<T>類里:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
  const ret = {} as Proxify<T>;
  for (const key in o) {
    ret[key] = {
      get() { return o[key]; },
      set(v: T[keyof T]) { console.log(v); }
    };
  }
  return ret;
}
let proxyProps = proxify(props);

注意 Readonly<T>Partial<T>用處不小,因此它們與 PickRecord一同被包含進了 TypeScript 的標准庫里:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string, T> = {
    [P in K]: T;
}

ReadonlyPartialPick是同態的,但 Record不是。 因為 Record並不需要輸入類型來拷貝屬性,所以它不屬於同態:
非同態類型本質上會創建新的屬性,因此它們不會從它處拷貝屬性修飾符。

由映射類型進行推斷

現在你了解了如何包裝一個類型的屬性,那么接下來就是如何拆包。 其實這也非常容易:

function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}

let originalProps = unproxify(proxyProps);

注意這個拆包推斷只適用於同態的映射類型。 如果映射類型不是同態的,那么需要給拆包函數一個明確的類型參數。

預定義的有條件類型

TypeScript 2.8 在lib.d.ts里增加了一些預定義的有條件類型:

  • Exclude<T, U> -- 從T中剔除可以賦值給U的類型。
  • Extract<T, U> -- 提取T中可以賦值給U的類型。
  • NonNullable<T> -- 從T中剔除nullundefined
  • ReturnType<T> -- 獲取函數返回值類型。
  • InstanceType<T> -- 獲取構造函數類型的實例類型。

以下為前文提到的預定條件類型
Readonly<T>給 T 的所有 key 加上 readonly 屬性
Partial<T>給 T 的所有值加上 undefined
Pick<T,K>從 T 中找出 key 包含在 K 中的屬性
Record<K,T>用 K 作為屬性,用 T 作為值,創建類型
Omit<T,K>在4.1.2版本中已經成為預定義條件類型,從 T 中剔除 key 包含在 K 中的鍵

示例

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {} 注釋:4.1.2中是 unknown
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any 注釋:4.1.2中是 never
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any 注釋:4.1.2中是 never
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

注意:Exclude類型是建議的Diff類型的一種實現。我們使用Exclude這個名字是為了避免破壞已經定義了Diff的代碼,並且我們感覺這個名字能更好地表達類型的語義。我們沒有增加Omit<T, K>類型,因為它可以很容易的用Pick<T, Exclude<keyof T, K>>來表示。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM