1. 程式人生 > 實用技巧 >TypeScript高階型別

TypeScript高階型別

交叉型別(Intersection Types)

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

我們大多是在混入(mixins)或其它不適合典型面向物件模型的地方看到交叉型別的使用。 (在JavaScript裡發生這種情況的場合很多!) 下面是如何建立混入的一個簡單例子:

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)

聯合型別與交叉型別很有關聯,但是使用上卻完全不同。 偶爾你會遇到這種情況,一個程式碼庫希望傳入 numberstring型別的引數。 例如下面的函式:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: any) {
    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}'.`);
}

padLeft("Hello world", 4); // returns "    Hello world"

padLeft存在一個問題, padding引數的型別指定成了 any。 這就是說我們可以傳入一個既不是 number也不是 string型別的引數,但是TypeScript卻不報錯。

let indentedString = padLeft("Hello world", true); // 編譯階段通過,執行時報錯

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

代替 any

, 我們可以使用 聯合型別做為 padding的引數:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

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

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

如果一個值是聯合型別,我們只能訪問此聯合型別的所有型別裡共有的成員。

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

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

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

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

這裡的聯合型別可能有點複雜,但是你很容易就習慣了。 如果一個值的型別是 A | B,我們能夠 確定的是它包含了 A B中共有的成員。 這個例子裡, Bird具有一個 fly成員。 我們不能確定一個 Bird | Fish型別的變數是否有 fly方法。 如果變數在執行時是 Fish型別,那麼呼叫 pet.fly()就出錯了。

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

聯合型別適合於那些值可以為不同型別的情況。 但當我們想確切地瞭解是否為 Fish時怎麼辦? JavaScript裡常用來區分2個可能值的方法是檢查成員是否存在。 如之前提及的,我們只能訪問聯合型別中共同擁有的成員。

let pet = getSmallPet();

// 每一個成員訪問都會報錯
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

為了讓這段程式碼工作,我們要使用型別斷言:

let pet = getSmallPet();

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

使用者自定義的型別保護

這裡可以注意到我們不得不多次使用型別斷言。 假若我們一旦檢查過型別,就能在之後的每個分支裡清楚地知道 pet的型別的話就好了。

TypeScript裡的 型別保護機制讓它成為了現實。 型別保護就是一些表示式,它們會在執行時檢查以確保在某個作用域裡的型別。 要定義一個型別保護,我們只要簡單地定義一個函式,它的返回值是一個 型別謂詞

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

注意TypeScript不僅知道在 if分支裡 petFish型別; 它還清楚在 else分支裡,一定 不是 Fish型別,一定是 Bird型別。

typeof型別保護

現在我們回過頭來看看怎麼使用聯合型別書寫 padLeft程式碼。 我們可以像下面這樣利用型別斷言來寫:

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

然而,必須要定義一個函式來判斷型別是否是原始型別,這太痛苦了。 幸運的是,現在我們不必將 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型別保護

如果你已經閱讀了 typeof型別保護並且對JavaScript裡的 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. 構造簽名所返回的型別的聯合

以此順序。

可以為null的型別

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

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

let s = "foo";
s = null; // 錯誤, 'null'不能賦值給'string'
let sn: string | null = "bar";
sn = null; // 可以

sn = undefined; // error, 'undefined'不能賦值給'string | null'

注意,按照JavaScript的語義,TypeScript會把 nullundefined區別對待。 string | nullstring | undefinedstring | undefined | null是不同的型別。

可選引數和可選屬性

使用了 --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;
    }
}

這裡很明顯地去除了 null,你也可以使用短路運算子:

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

如果編譯器不能夠去除 nullundefined,你可以使用型別斷言手動去除。 語法是新增 !字尾: identifier!identifier的型別裡去除了 nullundefined

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}

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;

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

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其它型別)。 因為 軟體中的物件應該對於擴充套件是開放的,但是對於修改是封閉的,你應該儘量去使用介面代替類型別名。

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

字串字面量型別

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

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

你只能從三種允許的字元中選擇其一來做為引數傳遞,傳入其它值則會產生錯誤。

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

字串字面量型別還可以用於區分函式過載:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

數字字面量型別

TypeScript還具有數字字面量型別。

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
}

我們很少直接這樣使用,但它們可以用在縮小範圍除錯bug的時候:

function foo(x: number) {
    if (x !== 1 || x !== 2) {
        //         ~~~~~~~
        // Operator '!==' cannot be applied to types '1' and '2'.
    }
}

換句話說,當 x2進行比較的時候,它的值必須為 1,這就意味著上面的比較檢查是非法的。

列舉成員型別

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

在我們談及“單例型別”的時候,多數是指列舉成員型別和數字/字串字面量型別,儘管大多數使用者會互換使用“單例型別”和“字面量型別”。

可辨識聯合(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型別,你可以繼承它,新的類可以直接使用之前的方法,不需要做任何的改變。

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();

如果沒有 this型別, ScientificCalculator就不能夠在繼承 BasicCalculator的同時還保持介面的連貫性。 multiply將會返回 BasicCalculator,它並沒有 sin方法。 然而,使用 this型別, multiply會返回 this,在這裡就是 ScientificCalculator

索引型別(Index types)

使用索引型別,編譯器就能夠檢查使用了動態屬性名的程式碼。 例如,一個常見的JavaScript模式是從物件中選取屬性的子集。

function pluck(o, names) {
    return names.map(n => o[n]);
}

下面是如何在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就可以了。 例如下面 getProperty函式的例子:

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

getProperty裡的 o: Tname: K,意味著 o[name]: T[K]。 當你返回 T[K]的結果,編譯器會例項化鍵的真實型別,因此 getProperty的返回值型別會隨著你需要的屬性改變。

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

索引型別和字串索引簽名

keyofT[K]與字串索引簽名進行互動。 如果你有一個帶有字串索引簽名的型別,那麼 keyof T會是 string。 並且 T[string]為索引簽名的型別:

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

對映型別

一個常見的任務是將一個已知的型別每個屬性都變為可選的:

interface PersonPartial {
    name?: string;
    age?: number;
}

或者我們想要一個只讀版本:

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

這在JavaScript裡經常出現,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>;

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

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

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

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

在個簡單的例子裡, Keys是硬編碼的的屬性名列表並且屬性型別永遠是 boolean,因此這個對映型別等同於:

type Flags = {
    option1: boolean;
    option2: boolean;
}

在真正的應用裡,可能不同於上面的 ReadonlyPartial。 它們會基於一些已存在的型別,且按照一定的方式轉換欄位。 這就是 keyof和索引訪問型別要做的事情:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

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

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.name也將是隻讀的且為可選的。

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

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> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

注意 ReadonlyPartial用處不小,因此它們與 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並不需要輸入型別來拷貝屬性,所以它不屬於同態:

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

非同態型別本質上會建立新的屬性,因此它們不會從它處拷貝屬性修飾符。

由對映型別進行推斷

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

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的型別。
  • Extract -- 提取T中可以賦值給U的型別。
  • NonNullable -- 從T中剔除nullundefined
  • ReturnType -- 獲取函式返回值型別。
  • InstanceType -- 獲取建構函式型別的例項型別。

示例

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)>;  // {}
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
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
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

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

詳細知識點見官方文件

TS官方文件