TS 學(xué)習(xí)指南(上)

一、TypeScript 是什么

TypeScript 是一種由微軟開發(fā)的自由和開源的編程語(yǔ)言。它是 JavaScript 的一個(gè)超集,而且本質(zhì)上向這個(gè)語(yǔ)言添加了可選的靜態(tài)類型和基于類的面向?qū)ο缶幊獭?/p>

TypeScript 提供最新的和不斷發(fā)展的 JavaScript 特性,包括那些來自 2015 年的 ECMAScript 和未來的提案中的特性,比如異步功能和 Decorators,以幫助建立健壯的組件。下圖顯示了 TypeScript 與 ES5、ES2015 和 ES2016 之間的關(guān)系:


1.1 TypeScript 與 JavaScript 的區(qū)別

1.2 獲取 TypeScript

命令行的 TypeScript 編譯器可以使用 npm 包管理器來安裝。

1.安裝 TypeScript
npm install -g typescript
2.驗(yàn)證 TypeScript
tsc -v 
Version 4.0.2
3.編譯 TypeScript 文件
$ tsc helloworld.ts
# helloworld.ts => helloworld.js

當(dāng)然,對(duì)剛?cè)腴T TypeScript 的小伙伴來說,也可以不用安裝 typescript,而是直接使用線上的 TypeScript Playground 來學(xué)習(xí)新的語(yǔ)法或新特性。通過配置 TS Config 的 Target,可以設(shè)置不同的編譯目標(biāo),從而編譯生成不同的目標(biāo)代碼。

TypeScript Playground

1.3 典型 TypeScript 工作流程

640.jpg

如你所見,在上圖中包含 3 個(gè) ts 文件:a.ts、b.ts 和 c.ts。這些文件將被 TypeScript 編譯器,根據(jù)配置的編譯選項(xiàng)編譯成 3 個(gè) js 文件,即 a.js、b.js 和 c.js。對(duì)于大多數(shù)使用 TypeScript 開發(fā)的 Web 項(xiàng)目,我們還會(huì)對(duì)編譯生成的 js 文件進(jìn)行打包處理,然后在進(jìn)行部署。

1.4 TypeScript 初體驗(yàn)

新建一個(gè) hello.ts 文件,并輸入以下內(nèi)容:

function greet(person: string) {
  return 'Hello, ' + person;
}
console.log(greet("TypeScript"));

然后執(zhí)行 tsc hello.ts 命令,之后會(huì)生成一個(gè)編譯好的文件 hello.js:

"use strict";
function greet(person) {
  return 'Hello, ' + person;
}
console.log(greet("TypeScript"));

觀察以上編譯后的輸出結(jié)果,我們發(fā)現(xiàn) person 參數(shù)的類型信息在編譯后被擦除了。TypeScript 只會(huì)在編譯階段對(duì)類型進(jìn)行靜態(tài)檢查,如果發(fā)現(xiàn)有錯(cuò)誤,編譯時(shí)就會(huì)報(bào)錯(cuò)。而在運(yùn)行時(shí),編譯生成的 JS 與普通的 JavaScript 文件一樣,并不會(huì)進(jìn)行類型檢查。

二、TypeScript 基礎(chǔ)類型

2.1 Boolean 類型

let isDone: boolean = false;
// ES5:var isDone = false;

2.2 Number 類型

let count: number = 10;
// ES5:var count = 10;

2.3 String 類型

let name: string = "semliker";
// ES5:var name = 'semlinker';

2.4 Symbol 類型

const sym = Symbol();
let obj = {
  [sym]: "semlinker",
};

console.log(obj[sym]); // semlinker

2.5 Array 類型

let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];

let list: Array<number> = [1, 2, 3]; // Array<number>泛型語(yǔ)法
// ES5:var list = [1,2,3];

2.6 Enum 類型

使用枚舉我們可以定義一些帶名字的常量。 使用枚舉可以清晰地表達(dá)意圖或創(chuàng)建一組有區(qū)別的用例。 TypeScript 支持?jǐn)?shù)字的和基于字符串的枚舉。

1.數(shù)字枚舉
enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH;

默認(rèn)情況下,NORTH 的初始值為 0,其余的成員會(huì)從 1 開始自動(dòng)增長(zhǎng)。換句話說,Direction.SOUTH 的值為 1,Direction.EAST 的值為 2,Direction.WEST 的值為 3。
以上的枚舉示例經(jīng)編譯后,對(duì)應(yīng)的 ES5 代碼如下:

"use strict";
var Direction;
(function (Direction) {
  Direction[(Direction["NORTH"] = 0)] = "NORTH";
  Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
  Direction[(Direction["EAST"] = 2)] = "EAST";
  Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH;

當(dāng)然我們也可以設(shè)置 NORTH 的初始值,比如:

enum Direction {
  NORTH = 3,
  SOUTH,
  EAST,
  WEST,
}
2.字符串枚舉

在 TypeScript 2.4 版本,允許我們使用字符串枚舉。在一個(gè)字符串枚舉里,每個(gè)成員都必須用字符串字面量,或另外一個(gè)字符串枚舉成員進(jìn)行初始化。

enum Direction {
  NORTH = "NORTH",
  SOUTH = "SOUTH",
  EAST = "EAST",
  WEST = "WEST",
}

以上代碼對(duì)應(yīng)的 ES5 代碼如下:

"use strict";
var Direction;
(function (Direction) {
    Direction["NORTH"] = "NORTH";
    Direction["SOUTH"] = "SOUTH";
    Direction["EAST"] = "EAST";
    Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));

通過觀察數(shù)字枚舉和字符串枚舉的編譯結(jié)果,我們可以知道數(shù)字枚舉除了支持 從成員名稱到成員值 的普通映射之外,它還支持 從成員值到成員名稱 的反向映射:

enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dirName = Direction[0]; // NORTH
let dirVal = Direction["NORTH"]; /

另外,對(duì)于純字符串枚舉,我們不能省略任何初始化程序。而數(shù)字枚舉如果沒有顯式設(shè)置值時(shí),則會(huì)使用默認(rèn)規(guī)則進(jìn)行初始化。

3.常量枚舉

除了數(shù)字枚舉和字符串枚舉之外,還有一種特殊的枚舉<常量枚舉>。它是使用 const 關(guān)鍵字修飾的枚舉,常量枚舉會(huì)使用內(nèi)聯(lián)語(yǔ)法,不會(huì)為枚舉類型編譯生成任何 JavaScript。為了更好地理解這句話,我們來看一個(gè)具體的例子:

const enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH;

以上代碼對(duì)應(yīng)的 ES5 代碼如下:

"use strict";
var dir = 0 /* NORTH */;
4.異構(gòu)枚舉

異構(gòu)枚舉的成員值是數(shù)字和字符串的混合:

enum Enum {
  A,
  B,
  C = "C",
  D = "D",
  E = 8,
  F,
}

以上代碼對(duì)于的 ES5 代碼如下:

"use strict";
var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
    Enum[Enum["B"] = 1] = "B";
    Enum["C"] = "C";
    Enum["D"] = "D";
    Enum[Enum["E"] = 8] = "E";
    Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {}));

通過觀察上述生成的 ES5 代碼,我們可以發(fā)現(xiàn)數(shù)字枚舉相對(duì)字符串枚舉多了 “反向映射”:

console.log(Enum.A) //輸出:0
console.log(Enum[0]) // 輸出:A
2.7 Any 類型

在 TypeScript 中,任何類型都可以被歸為 any 類型。這讓 any 類型成為了類型系統(tǒng)的頂級(jí)類型(也被稱作全局超級(jí)類型)。

let notSure: any = 666;
notSure = "semlinker";
notSure = false;

any 類型本質(zhì)上是類型系統(tǒng)的一個(gè)逃逸艙。作為開發(fā)者,這給了我們很大的自由:TypeScript 允許我們對(duì) any 類型的值執(zhí)行任何操作,而無(wú)需事先執(zhí)行任何形式的檢查。比如:

let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

在許多場(chǎng)景下,這太寬松了。使用 any 類型,可以很容易地編寫類型正確但在運(yùn)行時(shí)有問題的代碼。如果我們使用 any 類型,就無(wú)法使用 TypeScript 提供的大量的保護(hù)機(jī)制。為了解決 any 帶來的問題,TypeScript 3.0 引入了 unknown 類型。

2.8 Unknown 類型

就像所有類型都可以賦值給 any,所有類型也都可以賦值給 unknown。這使得 unknown 成為 TypeScript 類型系統(tǒng)的另一種頂級(jí)類型(另一種是 any)。下面我們來看一下 unknown 類型的使用示例:

let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK

對(duì) value 變量的所有賦值都被認(rèn)為是類型正確的。但是,當(dāng)我們嘗試將類型為 unknown 的值賦值給其他類型的變量時(shí)會(huì)發(fā)生什么?

let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

unknown 類型只能被賦值給 any 類型和 unknown 類型本身。直觀地說,這是有道理的:只有能夠保存任意類型值的容器才能保存 unknown 類型的值。畢竟我們不知道變量 value 中存儲(chǔ)了什么類型的值。

現(xiàn)在讓我們看看當(dāng)我們嘗試對(duì)類型為 unknown 的值執(zhí)行操作時(shí)會(huì)發(fā)生什么。以下是我們?cè)谥?any 章節(jié)看過的相同操作:

let value: unknown;

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

將 value 變量類型設(shè)置為 unknown 后,這些操作都不再被認(rèn)為是類型正確的。通過將 any 類型改變?yōu)?unknown 類型,我們已將允許所有更改的默認(rèn)設(shè)置,更改為禁止任何更改。

2.9 Tuple 類型

眾所周知,數(shù)組一般由同種類型的值組成,但有時(shí)我們需要在單個(gè)變量中存儲(chǔ)不同類型的值,這時(shí)候我們就可以使用元組。在 JavaScript 中是沒有元組的,元組是 TypeScript 中特有的類型,其工作方式類似于數(shù)組。

元組可用于定義具有有限數(shù)量的未命名屬性的類型。每個(gè)屬性都有一個(gè)關(guān)聯(lián)的類型。使用元組時(shí),必須提供每個(gè)屬性的值。為了更直觀地理解元組的概念,我們來看一個(gè)具體的例子:

let tupleType: [string, boolean];
tupleType = ["semlinker", true];

在上面代碼中,我們定義了一個(gè)名為 tupleType 的變量,它的類型是一個(gè)類型數(shù)組 [string, boolean],然后我們按照正確的類型依次初始化 tupleType 變量。與數(shù)組一樣,我們可以通過下標(biāo)來訪問元組中的元素:

console.log(tupleType[0]); // semlinker
console.log(tupleType[1]); // true

在元組初始化的時(shí)候,如果出現(xiàn)類型不匹配的話,比如:

tupleType = [true, "semlinker"];

此時(shí),TypeScript 編譯器會(huì)提示以下錯(cuò)誤信息:

[0]: Type 'true' is not assignable to type 'string'.
[1]: Type 'string' is not assignable to type 'boolean'.

很明顯是因?yàn)轭愋筒黄ヅ鋵?dǎo)致的。在元組初始化的時(shí)候,我們還必須提供每個(gè)屬性的值,不然也會(huì)出現(xiàn)錯(cuò)誤,比如:

tupleType = ["semlinker"];

此時(shí),TypeScript 編譯器會(huì)提示以下錯(cuò)誤信息:

Property '1' is missing in type '[string]' but required in type '[string, boolean]'.
2.10 Void 類型

某種程度上來說,void 類型像是與 any 類型相反,它表示沒有任何類型。當(dāng)一個(gè)函數(shù)沒有返回值時(shí),你通常會(huì)見到其返回值類型是 void:

// 聲明函數(shù)返回值為void
function warnUser(): void {
  console.log("This is my warning message");
}

以上代碼編譯生成的 ES5 代碼如下:

"use strict";
function warnUser() {
  console.log("This is my warning message");
}

需要注意的是,聲明一個(gè) void 類型的變量沒有什么作用,因?yàn)樗闹抵荒転?undefined 或 null:

let unusable: void = undefined;
2.11 Null 和 Undefined 類型

TypeScript 里,undefined 和 null 兩者有各自的類型分別為 undefined 和 null。

let u: undefined = undefined;
let n: null = null;

默認(rèn)情況下 null 和 undefined 是所有類型的子類型。 就是說你可以把 null 和 undefined 賦值給 number 類型的變量。然而,如果你指定了--strictNullChecks 標(biāo)記,null 和 undefined 只能賦值給 void 和它們各自的類型。

2.12 object, Object 和 {} 類型

1.object 類型

object 類型是:TypeScript 2.2 引入的新類型,它用于表示非原始類型。

// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  create(o: object | null): any;
  // ...
}

const proto = {};

Object.create(proto);     // OK
Object.create(null);      // OK
Object.create(undefined); // Error
Object.create(1337);      // Error
Object.create(true);      // Error
Object.create("oops");    // Error
2.Object 類型

Object 類型:它是所有 Object 類的實(shí)例的類型,它由以下兩個(gè)接口來定義:

  • Object 接口定義了 Object.prototype 原型對(duì)象上的屬性;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  hasOwnProperty(v: PropertyKey): boolean;
  isPrototypeOf(v: Object): boolean;
  propertyIsEnumerable(v: PropertyKey): boolean;
}
  • ObjectConstructor 接口定義了 Object 類的屬性。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
  /** Invocation via `new` */
  new(value?: any): Object;
  /** Invocation via function calls */
  (value?: any): any;
  readonly prototype: Object;
  getPrototypeOf(o: any): any;
  // ···
}

declare var Object: ObjectConstructor;

Object 類的所有實(shí)例都繼承了 Object 接口中的所有屬性。

3.{} 類型

{} 類型描述了一個(gè)沒有成員的對(duì)象。當(dāng)你試圖訪問這樣一個(gè)對(duì)象的任意屬性時(shí),TypeScript 會(huì)產(chǎn)生一個(gè)編譯時(shí)錯(cuò)誤。

// Type {}
const obj = {};

// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";

但是,你仍然可以使用在 Object 類型上定義的所有屬性和方法,這些屬性和方法可通過 JavaScript 的原型鏈隱式地使用:

// Type {}
const obj = {};

// "[object Object]"
obj.toString();
2.13 Never 類型

never 類型表示的是那些永不存在的值的類型。 例如,never 類型是那些總是會(huì)拋出異?;蚋揪筒粫?huì)有返回值的函數(shù)表達(dá)式或箭頭函數(shù)表達(dá)式的返回值類型。

// 返回never的函數(shù)必須存在無(wú)法達(dá)到的終點(diǎn)
function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

在 TypeScript 中,可以利用 never 類型的特性來實(shí)現(xiàn)全面性檢查,具體示例如下:

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 這里 foo 被收窄為 string 類型
  } else if (typeof foo === "number") {
    // 這里 foo 被收窄為 number 類型
  } else {
    // foo 在這里是 never
    const check: never = foo;
  }
}

注意在 else 分支里面,我們把收窄為 never 的 foo 賦值給一個(gè)顯示聲明的 never 變量。如果一切邏輯正確,那么這里應(yīng)該能夠編譯通過。但是假如后來有一天你的同事修改了 Foo 的類型:

type Foo = string | number | boolean;

然而他忘記同時(shí)修改 controlFlowAnalysisWithNever 方法中的控制流程,這時(shí)候 else 分支的 foo 類型會(huì)被收窄為 boolean 類型,導(dǎo)致無(wú)法賦值給 never 類型,這時(shí)就會(huì)產(chǎn)生一個(gè)編譯錯(cuò)誤。通過這個(gè)方式,我們可以確保

controlFlowAnalysisWithNever 方法總是窮盡了 Foo 的所有可能類型。 通過這個(gè)示例,我們可以得出一個(gè)結(jié)論:使用 never 避免出現(xiàn)新增了聯(lián)合類型沒有對(duì)應(yīng)的實(shí)現(xiàn),目的就是寫出類型絕對(duì)安全的代碼。

三、TypeScript 斷言

3.1 類型斷言

有時(shí)候你會(huì)遇到這樣的情況,你會(huì)比 TypeScript 更了解某個(gè)值的詳細(xì)信息。通常這會(huì)發(fā)生在你清楚地知道一個(gè)實(shí)體具有比它現(xiàn)有類型更確切的類型。

通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在干什么”。類型斷言好比其他語(yǔ)言里的類型轉(zhuǎn)換,但是不進(jìn)行特殊的數(shù)據(jù)檢查和解構(gòu)。它沒有運(yùn)行時(shí)的影響,只是在編譯階段起作用。
類型斷言形式:

  • as 語(yǔ)法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

3.2 非空斷言

在上下文中當(dāng)類型檢查器無(wú)法斷定類型時(shí),一個(gè)新的后綴表達(dá)式操作符 ! 可以用于斷言操作對(duì)象是非 null 和非 undefined 類型。具體而言,x! 將從 x 值域中排除 null 和 undefined 。

那么非空斷言操作符到底有什么用呢?下面我們先來看一下非空斷言操作符的一些使用場(chǎng)景。

  • 1.忽略 undefined 和 null 類型
function myFunc(maybeString: string | undefined | null) {
  // Type 'string | null | undefined' is not assignable to type 'string'.
  // Type 'undefined' is not assignable to type 'string'. 
  const onlyString: string = maybeString; // Error
  const ignoreUndefinedAndNull: string = maybeString!; // Ok
}
  • 2.調(diào)用函數(shù)時(shí)忽略 undefined 類型
type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
  // Object is possibly 'undefined'.(2532)
  // Cannot invoke an object which is possibly 'undefined'.(2722)
  const num1 = numGenerator(); // Error
  const num2 = numGenerator!(); //OK
}

因?yàn)?! 非空斷言操作符會(huì)從編譯生成的 JavaScript 代碼中移除,所以在實(shí)際使用的過程中,要特別注意。比如下面這個(gè)例子:

const a: number | undefined = undefined;
const b: number = a!;
console.log(b); 

以上 TS 代碼會(huì)編譯生成以下 ES5 代碼:

"use strict";
const a = undefined;
const b = a;
console.log(b);

雖然在 TS 代碼中,我們使用了非空斷言,使得 const b: number = a!; 語(yǔ)句可以通過 TypeScript 類型檢查器的檢查。但在生成的 ES5 代碼中,! 非空斷言操作符被移除了,所以在瀏覽器中執(zhí)行以上代碼,在控制臺(tái)會(huì)輸出 undefined。

3.3 確定賦值斷言

在 TypeScript 2.7 版本中引入了確定賦值斷言,即允許在實(shí)例屬性和變量聲明后面放置一個(gè) ! 號(hào),從而告訴 TypeScript 該屬性會(huì)被明確地賦值。為了更好地理解它的作用,我們來看個(gè)具體的例子:

let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error

function initialize() {
  x = 10;
}

很明顯該異常信息是說變量 x 在賦值前被使用了,要解決該問題,我們可以使用確定賦值斷言:

let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
  x = 10;
}

通過 let x!: number; 確定賦值斷言,TypeScript 編譯器就會(huì)知道該屬性會(huì)被明確地賦值。

四、類型守衛(wèi)

類型保護(hù)是可執(zhí)行運(yùn)行時(shí)檢查的一種表達(dá)式,用于確保該類型在一定的范圍內(nèi)。 換句話說,類型保護(hù)可以保證一個(gè)字符串是一個(gè)字符串,盡管它的值也可以是一個(gè)數(shù)值。類型保護(hù)與特性檢測(cè)并不是完全不同,其主要思想是嘗試檢測(cè)屬性、方法或原型,以確定如何處理值。目前主要有四種的方式來實(shí)現(xiàn)類型保護(hù):

4.1 in 關(guān)鍵字
interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}
4.2 typeof 關(guān)鍵字
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 并不會(huì)阻止你與其它字符串比較,語(yǔ)言不會(huì)把那些表達(dá)式識(shí)別為類型保護(hù)。

4.3 instanceof 關(guān)鍵字
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;
  }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
  // padder的類型收窄為 'SpaceRepeatingPadder'
}
4.4 自定義類型保護(hù)的類型謂詞
function isNumber(x: any): x is number {
  return typeof x === "number";
}

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

五、聯(lián)合類型和類型別名

5.1 聯(lián)合類型

聯(lián)合類型通常與 null 或 undefined 一起使用:

const sayHello = (name: string | undefined) => {
  /* ... */
};

例如,這里 name 的類型是 string | undefined 意味著可以將 string 或 undefined 的值傳遞給sayHello 函數(shù)。

sayHello("semlinker");
sayHello(undefined);

通過這個(gè)示例,你可以憑直覺知道類型 A 和類型 B 聯(lián)合后的類型是同時(shí)接受 A 和 B 值的類型。此外,對(duì)于聯(lián)合類型來說,你可能會(huì)遇到以下的用法:

let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';

以上示例中的 1、2 或 'click' 被稱為字面量類型,用來約束取值只能是某幾個(gè)值中的一個(gè)。

5.2 可辨識(shí)聯(lián)合

TypeScript 可辨識(shí)聯(lián)合(Discriminated Unions)類型,也稱為代數(shù)數(shù)據(jù)類型或標(biāo)簽聯(lián)合類型。它包含 3 個(gè)要點(diǎn):可辨識(shí)、聯(lián)合類型和類型守衛(wèi)。

這種類型的本質(zhì)是結(jié)合聯(lián)合類型和字面量類型的一種類型保護(hù)方法。如果一個(gè)類型是多個(gè)類型的聯(lián)合類型,且多個(gè)類型含有一個(gè)公共屬性,那么就可以利用這個(gè)公共屬性,來創(chuàng)建不同的類型保護(hù)區(qū)塊。

1.可辨識(shí)
可辨識(shí)要求聯(lián)合類型中的每個(gè)元素都含有一個(gè)單例類型屬性,比如:

enum CarTransmission {
  Automatic = 200,
  Manual = 300
}

interface Motorcycle {
  vType: "motorcycle"; // discriminant
  make: number; // year
}

interface Car {
  vType: "car"; // discriminant
  transmission: CarTransmission
}

interface Truck {
  vType: "truck"; // discriminant
  capacity: number; // in tons
}

在上述代碼中,我們分別定義了 Motorcycle、 Car 和 Truck 三個(gè)接口,在這些接口中都包含一個(gè) vType 屬性,該屬性被稱為可辨識(shí)的屬性,而其它的屬性只跟特性的接口相關(guān)。
2.聯(lián)合類型
基于前面定義了三個(gè)接口,我們可以創(chuàng)建一個(gè) Vehicle 聯(lián)合類型:

type Vehicle = Motorcycle | Car | Truck;

現(xiàn)在我們就可以開始使用 Vehicle 聯(lián)合類型,對(duì)于 Vehicle 類型的變量,它可以表示不同類型的車輛。
3.類型守衛(wèi)
下面我們來定義一個(gè) evaluatePrice 方法,該方法用于根據(jù)車輛的類型、容量和評(píng)估因子來計(jì)算價(jià)格,具體實(shí)現(xiàn)如下:

const EVALUATION_FACTOR = Math.PI; 

function evaluatePrice(vehicle: Vehicle) {
  return vehicle.capacity * EVALUATION_FACTOR;
}

const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);

對(duì)于以上代碼,TypeScript 編譯器將會(huì)提示以下錯(cuò)誤信息:

Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.

原因是在 Motorcycle 接口中,并不存在 capacity 屬性,而對(duì)于 Car 接口來說,它也不存在 capacity 屬性。那么,現(xiàn)在我們應(yīng)該如何解決以上問題呢?這時(shí),我們可以使用類型守衛(wèi)。下面我們來重構(gòu)一下前面定義的 evaluatePrice 方法,重構(gòu)后的代碼如下:

function evaluatePrice(vehicle: Vehicle) {
  switch(vehicle.vType) {
    case "car":
      return vehicle.transmission * EVALUATION_FACTOR;
    case "truck":
      return vehicle.capacity * EVALUATION_FACTOR;
    case "motorcycle":
      return vehicle.make * EVALUATION_FACTOR;
  }
}

在以上代碼中,我們使用 switch 和 case 運(yùn)算符來實(shí)現(xiàn)類型守衛(wèi),從而確保在 evaluatePrice 方法中,我們可以安全地訪問 vehicle 對(duì)象中的所包含的屬性,來正確的計(jì)算該車輛類型所對(duì)應(yīng)的價(jià)格。

5.3 類型別名
類型別名用來給一個(gè)類型起個(gè)新名字。

type Message = string | string[];

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

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

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