高級類型

高級類型

官方文檔

交叉類型(Intersection Types)

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

我們大多是在混入(mixins)或其它不適合典型面向?qū)ο竽P偷牡胤娇吹浇徊骖愋偷氖褂谩?(在 JavaScript 里發(fā)生這種情況的場合很多!) 下面是如何創(chuàng)建混入的一個簡單例子:

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

聯(lián)合類型(Union Types)

聯(lián)合類型與交叉類型很有關(guān)聯(lián),但是使用上卻完全不同。 偶爾你會遇到這種情況,一個代碼庫希望傳入 numberstring類型的參數(shù)。 例如下面的函數(shù):

/**
 * 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參數(shù)的類型指定成了 any。 這就是說我們可以傳入一個既不是 number也不是 string類型的參數(shù),但是 TypeScript 卻不報錯。

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

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

代替 any, 我們可以使用 聯(lián)合類型做為 padding的參數(shù):

/**
 * 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

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

如果一個值是聯(lián)合類型,我們只能訪問此聯(lián)合類型的所有類型里共有的成員。

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

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

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

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

這里的聯(lián)合類型可能有點復(fù)雜,但是你很容易就習(xí)慣了。 如果一個值的類型是 A | B,我們能夠 確定的是它包含了 A B中共有的成員。 這個例子里, Bird具有一個 fly成員。 我們不能確定一個 Bird | Fish類型的變量是否有 fly方法。 如果變量在運行時是 Fish類型,那么調(diào)用 pet.fly()就出錯了。

類型保護(hù)與區(qū)分類型(Type Guards and Differentiating Types)

聯(lián)合類型適合于那些值可以為不同類型的情況。 但當(dāng)我們想確切地了解是否為 Fish時怎么辦? JavaScript 里常用來區(qū)分 2 個可能值的方法是檢查成員是否存在。 如之前提及的,我們只能訪問聯(lián)合類型中共同擁有的成員。

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

用戶自定義的類型保護(hù)

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

TypeScript 里的 類型保護(hù)機(jī)制讓它成為了現(xiàn)實。 類型保護(hù)就是一些表達(dá)式,它們會在運行時檢查以確保在某個作用域里的類型。 要定義一個類型保護(hù),我們只要簡單地定義一個函數(shù),它的返回值是一個 類型謂詞

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

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

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

// 'swim' 和 'fly' 調(diào)用都沒有問題了

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

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

typeof類型保護(hù)

現(xiàn)在我們回過頭來看看怎么使用聯(lián)合類型書寫 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}'.`);
}

然而,必須要定義一個函數(shù)來判斷類型是否是原始類型,這太痛苦了。 幸運的是,現(xiàn)在我們不必將 typeof x === "number"抽象成一個函數(shù),因為 TypeScript 可以將它識別為一個類型保護(hù)。 也就是說我們可以直接在代碼里檢查類型了。

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類型保護(hù)*只有兩種形式能被識別: typeof v === "typename"typeof v !== "typename", "typename"必須是 "number", "string", "boolean""symbol"。 但是 TypeScript 并不會阻止你與其它字符串比較,語言不會把那些表達(dá)式識別為類型保護(hù)。

instanceof類型保護(hù)

如果你已經(jīng)閱讀了 typeof類型保護(hù)并且對 JavaScript 里的 instanceof操作符熟悉的話,你可能已經(jīng)猜到了這節(jié)要講的內(nèi)容。

instanceof類型保護(hù)是通過構(gòu)造函數(shù)來細(xì)化類型的一種方式。 比如,我們借鑒一下之前字符串填充的例子:

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; // 類型細(xì)化為'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
  padder; // 類型細(xì)化為'StringPadder'
}

instanceof的右側(cè)要求是一個構(gòu)造函數(shù),TypeScript 將細(xì)化為:

  1. 此構(gòu)造函數(shù)的 prototype屬性的類型,如果它的類型不為 any的話
  2. 構(gòu)造簽名所返回的類型的聯(lián)合

以此順序。

可以為 null 的類型

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

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

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

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

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

可選參數(shù)和可選屬性

使用了 --strictNullChecks,可選參數(shù)會被自動地加上 | 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'

類型保護(hù)和類型斷言

由于可以為 null 的類型是通過聯(lián)合類型實現(xiàn),那么你需要使用類型保護(hù)來去除 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");
}

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

類型別名

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

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

起別名不會新建一個類型 - 它創(chuàng)建了一個新 名字來引用那個類型。 給原始類型起別名通常沒什么用,盡管可以做為文檔的一種形式使用。

同接口一樣,類型別名也可以是泛型 - 我們可以添加類型參數(shù)并且在別名聲明的右側(cè)傳入:

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

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

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

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

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;

然而,類型別名不能出現(xiàn)在聲明右側(cè)的任何地方。

type Yikes = Array<Yikes>; // error

接口 vs. 類型別名

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

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

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

另一個重要區(qū)別是類型別名不能被 extendsimplements(自己也不能 extendsimplements其它類型)。 因為 軟件中的對象應(yīng)該對于擴(kuò)展是開放的,但是對于修改是封閉的,你應(yīng)該盡量去使用接口代替類型別名。

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

字符串字面量類型

字符串字面量類型允許你指定字符串必須的固定值。 在實際應(yīng)用中,字符串字面量類型可以與聯(lián)合類型,類型保護(hù)和類型別名很好的配合。 通過結(jié)合使用這些特性,你可以實現(xiàn)類似枚舉類型的字符串。

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

你只能從三種允許的字符中選擇其一來做為參數(shù)傳遞,傳入其它值則會產(chǎn)生錯誤。

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

字符串字面量類型還可以用于區(qū)分函數(shù)重載:

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

數(shù)字字面量類型

TypeScript 還具有數(shù)字字面量類型。

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

我們很少直接這樣使用,但它們可以用在縮小范圍調(diào)試 bug 的時候:

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

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

枚舉成員類型

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

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

可辨識聯(lián)合(Discriminated Unions)

你可以合并單例類型,聯(lián)合類型,類型保護(hù)和類型別名來創(chuàng)建一個叫做 可辨識聯(lián)合的高級模式,它也稱做 標(biāo)簽聯(lián)合代數(shù)數(shù)據(jù)類型。 可辨識聯(lián)合在函數(shù)式編程很有用處。 一些語言會自動地為你辨識聯(lián)合;而 TypeScript 則基于已有的 JavaScript 模式。 它具有 3 個要素:

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

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

type Shape = Square | Rectangle | Circle;

現(xiàn)在我們使用可辨識聯(lián)合:

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

完整性檢查

當(dāng)沒有涵蓋所有可辨識聯(lián)合的變化時,我們想讓編譯器可以通知我們。 比如,如果我們添加了 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"
}

有兩種方式可以實現(xiàn)。 首先是啟用 --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 認(rèn)為這個函數(shù)有時候會返回 undefined。 如果你明確地指定了返回值類型為 number,那么你會看到一個錯誤,因為實際上返回值的類型為 number | undefined。 然而,這種方法存在些微妙之處且 --strictNullChecks對舊代碼支持不好。

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

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將具有一個真實的類型并且你會得到一個錯誤。 這種方式需要你定義一個額外的函數(shù),但是在你忘記某個 case 的時候也更加明顯。

多態(tài)的 this類型

多態(tài)的 this類型表示的是某個包含類或接口的 子類型。 這被稱做 F-bounded 多態(tài)性。 它能很容易的表現(xiàn)連貫接口間的繼承,比如。 在計算器的例子里,在每個操作之后都返回 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)

使用索引類型,編譯器就能夠檢查使用了動態(tài)屬性名的代碼。 例如,一個常見的 JavaScript 模式是從對象中選取屬性的子集。

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

下面是如何在 TypeScript 里使用此函數(shù),通過 索引類型查詢索引訪問操作符:

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的結(jié)果為 T上已知的公共屬性名的聯(lián)合。 例如:

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

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

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

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

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]。 當(dāng)你返回 T[K]的結(jié)果,編譯器會實例化鍵的真實類型,因此 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]與字符串索引簽名進(jìn)行交互。 如果你有一個帶有字符串索引簽名的類型,那么 keyof T會是 string。 并且 T[string]為索引簽名的類型:

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

映射類型

一個常見的任務(wù)是將一個已知的類型每個屬性都變?yōu)榭蛇x的:

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

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

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

這在 JavaScript 里經(jīng)常出現(xiàn),TypeScript 提供了從舊類型中創(chuàng)建新類型的一種方式 — 映射類型。 在映射類型里,新類型以相同的形式去轉(zhuǎn)換舊類型里每個屬性。 例如,你可以令每個屬性成為 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 };

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

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

在個簡單的例子里, Keys是硬編碼的的屬性名列表并且屬性類型永遠(yuǎn)是 boolean,因此這個映射類型等同于:

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

在真正的應(yīng)用里,可能不同于上面的 ReadonlyPartial。 它們會基于一些已存在的類型,且按照一定的方式轉(zhuǎn)換字段。 這就是 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且結(jié)果類型是 T[P]的變體。 這是使用通用映射類型的一個好模版。 因為這類轉(zhuǎn)換是 同態(tài)的,映射只作用于 T的屬性而沒有其它的。 編譯器知道在添加任何新屬性之前可以拷貝所有存在的屬性修飾符。 例如,假設(shè) 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> {
  // ... wrap proxies ...
}
let proxyProps = proxify(props);

注意 Readonly<T>Partial<T>用處不小,因此它們與 PickRecord一同被包含進(jìn)了 TypeScript 的標(biāo)準(zhǔn)庫里:

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

ReadonlyPartialPick是同態(tài)的,但 Record不是。 因為 Record并不需要輸入類型來拷貝屬性,所以它不屬于同態(tài):

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

非同態(tài)類型本質(zhì)上會創(chuàng)建新的屬性,因此它們不會從它處拷貝屬性修飾符。

由映射類型進(jìn)行推斷

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

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

注意這個拆包推斷只適用于同態(tài)的映射類型。 如果映射類型不是同態(tài)的,那么需要給拆包函數(shù)一個明確的類型參數(shù)。

預(yù)定義的有條件類型

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

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

示例

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類型的一種實現(xiàn)。我們使用Exclude這個名字是為了避免破壞已經(jīng)定義了Diff的代碼,并且我們感覺這個名字能更好地表達(dá)類型的語義。我們沒有增加Omit<T, K>類型,因為它可以很容易的用Pick<T, Exclude<keyof T, K>>來表示。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、交叉類型 交叉類型將多個類型合并為一個新的類型,新的具有所有參與合并的類型的特性,本質(zhì)上是一種并的操作。形式如...
    kuxingseng686閱讀 648評論 0 0
  • 交叉類型 交叉類型將多個類型合并為一個類型,相當(dāng)于新類型具有這多個類型的所有特性,相當(dāng)于是一種并的操作,通常在使用...
    這個前端不太冷閱讀 710評論 0 0
  • 直達(dá) 第一課, 體驗typescript 第二課, 基礎(chǔ)類型和入門高級類型 第三課, 泛型 第四課, 解讀高級類型...
    鐵皮飯盒閱讀 5,289評論 0 60
  • # 類型別名 type 類型別名就是給已有的類型取一個新名字,并不會新建類型 類型別名:可以用于原始值,聯(lián)合類型,...
    woow_wu7閱讀 21,159評論 0 8
  • 可以為null的類型 TypeScript具有兩種特殊的類型, null和 undefined,它們分別具有值nu...
    2o壹9閱讀 1,051評論 0 49

友情鏈接更多精彩內(nèi)容