一份不可多得的 TS 學習指南(1.8W字)

一十偶、TypeScript 是什么

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

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

1.1 TypeScript 與 JavaScript 的區(qū)別

TypeScript JavaScript
JavaScript 的超集用于解決大型項目的代碼復雜性 一種腳本語言界逛,用于創(chuàng)建動態(tài)網(wǎng)頁
可以在編譯期間發(fā)現(xiàn)并糾正錯誤 作為一種解釋型語言昆稿,只能在運行時發(fā)現(xiàn)錯誤
強類型,支持靜態(tài)和動態(tài)類型 弱類型息拜,沒有靜態(tài)類型選項
最終被編譯成 JavaScript 代碼溉潭,使瀏覽器可以理解 可以直接在瀏覽器中使用
支持模塊、泛型和接口 不支持模塊少欺,泛型或接口
社區(qū)的支持仍在增長喳瓣,而且還不是很大 大量的社區(qū)支持以及大量文檔和解決問題的支持

1.2 獲取 TypeScript

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

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

當然赞别,對剛?cè)腴T TypeScript 的小伙伴來說畏陕,也可以不用安裝 typescript,而是直接使用線上的 TypeScript Playground 來學習新的語法或新特性仿滔。通過配置 TS Config 的 Target惠毁,可以設(shè)置不同的編譯目標,從而編譯生成不同的目標代碼崎页。

下圖示例中所設(shè)置的編譯目標是 ES5:

(圖片來源:https://www.typescriptlang.org/play

1.3 典型 TypeScript 工作流程

如你所見鞠绰,在上圖中包含 3 個 ts 文件:a.ts、b.ts 和 c.ts飒焦。這些文件將被 TypeScript 編譯器蜈膨,根據(jù)配置的編譯選項編譯成 3 個 js 文件,即 a.js牺荠、b.js 和 c.js丈挟。對于大多數(shù)使用 TypeScript 開發(fā)的 Web 項目,我們還會對編譯生成的 js 文件進行打包處理志电,然后在進行部署曙咽。

1.4 TypeScript 初體驗

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

function greet(person: string) {
  return 'Hello, ' + person;
}

console.log(greet("TypeScript"));

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

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

觀察以上編譯后的輸出結(jié)果例朱,我們發(fā)現(xiàn) person 參數(shù)的類型信息在編譯后被擦除了。TypeScript 只會在編譯階段對類型進行靜態(tài)檢查鱼蝉,如果發(fā)現(xiàn)有錯誤洒嗤,編譯時就會報錯。而在運行時魁亦,編譯生成的 JS 與普通的 JavaScript 文件一樣渔隶,并不會進行類型檢查。

二、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>泛型語法
// ES5:var list = [1,2,3];

2.6 Enum 類型

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

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

let dir: Direction = Direction.NORTH;

默認情況下,NORTH 的初始值為 0,其余的成員會從 1 開始自動增長数尿。換句話說,Direction.SOUTH 的值為 1军掂,Direction.EAST 的值為 2,Direction.WEST 的值為 3昨悼。

以上的枚舉示例經(jīng)編譯后蝗锥,對應(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;

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

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

在 TypeScript 2.4 版本率触,允許我們使用字符串枚舉终议。在一個字符串枚舉里,每個成員都必須用字符串字面量闲延,或另外一個字符串枚舉成員進行初始化。

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

以上代碼對應(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"]; // 0

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

3.常量枚舉

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

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

let dir: Direction = Direction.NORTH;

以上代碼對應(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,
}

以上代碼對于的 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ù)字枚舉相對字符串枚舉多了 “反向映射”:

console.log(Enum.A) //輸出:0
console.log(Enum[0]) // 輸出:A

2.7 Any 類型

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

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

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

let value: any;

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

在許多場景下玫氢,這太寬松了帚屉。使用 any 類型谜诫,可以很容易地編寫類型正確但在運行時有問題的代碼。如果我們使用 any 類型涮阔,就無法使用 TypeScript 提供的大量的保護機制猜绣。為了解決 any 帶來的問題,TypeScript 3.0 引入了 unknown 類型敬特。

2.8 Unknown 類型

就像所有類型都可以賦值給 any掰邢,所有類型也都可以賦值給 unknown。這使得 unknown 成為 TypeScript 類型系統(tǒng)的另一種頂級類型(另一種是 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

value 變量的所有賦值都被認為是類型正確的辣之。但是,當我們嘗試將類型為 unknown 的值賦值給其他類型的變量時會發(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 中存儲了什么類型的值多搀。

現(xiàn)在讓我們看看當我們嘗試對類型為 unknown 的值執(zhí)行操作時會發(fā)生什么。以下是我們在之前 any 章節(jié)看過的相同操作:

let value: unknown;

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

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

2.9 Tuple 類型

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

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

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

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

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

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

tupleType = [true, "semlinker"];

此時,TypeScript 編譯器會提示以下錯誤信息:

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

很明顯是因為類型不匹配導致的隙姿。在元組初始化的時候梅垄,我們還必須提供每個屬性的值,不然也會出現(xiàn)錯誤输玷,比如:

tupleType = ["semlinker"];

此時队丝,TypeScript 編譯器會提示以下錯誤信息:

Property '1' is missing in type '[string]' but required in type '[string, boolean]'.

2.10 Void 類型

某種程度上來說,void 類型像是與 any 類型相反欲鹏,它表示沒有任何類型机久。當一個函數(shù)沒有返回值時,你通常會見到其返回值類型是 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");
}

需要注意的是赔嚎,聲明一個 void 類型的變量沒有什么作用膘盖,因為在嚴格模式下,它的值只能為 undefined

let unusable: void = undefined;

2.11 Null 和 Undefined 類型

TypeScript 里尤误,undefinednull 兩者有各自的類型分別為 undefinednull侠畔。

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

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 類的實例的類型软棺,它由以下兩個接口來定義:

  • Object 接口定義了 Object.prototype 原型對象上的屬性;
// 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 類的所有實例都繼承了 Object 接口中的所有屬性码党。

3.{} 類型

{} 類型描述了一個沒有成員的對象德崭。當你試圖訪問這樣一個對象的任意屬性時斥黑,TypeScript 會產(chǎn)生一個編譯時錯誤。

// 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 類型是那些總是會拋出異陈故瘢或根本就不會有返回值的函數(shù)表達式或箭頭函數(shù)表達式的返回值類型。

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

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

在 TypeScript 中服球,可以利用 never 類型的特性來實現(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 賦值給一個顯示聲明的 never 變量斩熊。如果一切邏輯正確往枣,那么這里應(yīng)該能夠編譯通過。但是假如后來有一天你的同事修改了 Foo 的類型:

type Foo = string | number | boolean;

然而他忘記同時修改 controlFlowAnalysisWithNever 方法中的控制流程,這時候 else 分支的 foo 類型會被收窄為 boolean 類型分冈,導致無法賦值給 never 類型圾另,這時就會產(chǎn)生一個編譯錯誤。通過這個方式雕沉,我們可以確保

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

三、TypeScript 斷言

3.1 類型斷言

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

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

類型斷言有兩種形式:

1.“尖括號” 語法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
2.as 語法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

3.2 非空斷言

在上下文中當類型檢查器無法斷定類型時腹鹉,一個新的后綴表達式操作符 ! 可以用于斷言操作對象是非 null 和非 undefined 類型。具體而言敷硅,x! 將從 x 值域中排除 null 和 undefined 功咒。

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

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ù)時忽略 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
}

因為 ! 非空斷言操作符會從編譯生成的 JavaScript 代碼中移除力奋,所以在實際使用的過程中,要特別注意幽七。比如下面這個例子:

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

以上 TS 代碼會編譯生成以下 ES5 代碼:

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

雖然在 TS 代碼中景殷,我們使用了非空斷言,使得 const b: number = a!; 語句可以通過 TypeScript 類型檢查器的檢查澡屡。但在生成的 ES5 代碼中猿挚,! 非空斷言操作符被移除了,所以在瀏覽器中執(zhí)行以上代碼驶鹉,在控制臺會輸出 undefined绩蜻。

3.3 確定賦值斷言

在 TypeScript 2.7 版本中引入了確定賦值斷言,即允許在實例屬性和變量聲明后面放置一個 ! 號室埋,從而告訴 TypeScript 該屬性會被明確地賦值办绝。為了更好地理解它的作用踏兜,我們來看個具體的例子:

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 編譯器就會知道該屬性會被明確地賦值。

四昔驱、類型守衛(wèi)

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

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 類型保護只支持兩種形式:typeof v === "typename"typeof v !== typename巾腕,"typename" 必須是 "number""string"絮蒿, "boolean""symbol"尊搬。 但是 TypeScript 并不會阻止你與其它字符串比較,語言不會把那些表達式識別為類型保護土涝。

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 自定義類型保護的類型謂詞

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)合類型通常與 nullundefined 一起使用:

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

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

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

通過這個示例冀泻,你可以憑直覺知道類型 A 和類型 B 聯(lián)合后的類型是同時接受 A 和 B 值的類型。此外蜡饵,對于聯(lián)合類型來說弹渔,你可能會遇到以下的用法:

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

以上示例中的 12'click' 被稱為字面量類型验残,用來約束取值只能是某幾個值中的一個捞附。

5.2 可辨識聯(lián)合

TypeScript 可辨識聯(lián)合(Discriminated Unions)類型巾乳,也稱為代數(shù)數(shù)據(jù)類型或標簽聯(lián)合類型您没。它包含 3 個要點:可辨識、聯(lián)合類型和類型守衛(wèi)胆绊。

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

1.可辨識

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

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舔糖、 CarTruck 三個接口,在這些接口中都包含一個 vType 屬性莺匠,該屬性被稱為可辨識的屬性金吗,而其它的屬性只跟特性的接口相關(guān)。

2.聯(lián)合類型

基于前面定義了三個接口趣竣,我們可以創(chuàng)建一個 Vehicle 聯(lián)合類型:

type Vehicle = Motorcycle | Car | Truck;

現(xiàn)在我們就可以開始使用 Vehicle 聯(lián)合類型摇庙,對于 Vehicle 類型的變量,它可以表示不同類型的車輛遥缕。

3.類型守衛(wèi)

下面我們來定義一個 evaluatePrice 方法卫袒,該方法用于根據(jù)車輛的類型、容量和評估因子來計算價格单匣,具體實現(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);

對于以上代碼夕凝,TypeScript 編譯器將會提示以下錯誤信息:

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

原因是在 Motorcycle 接口中,并不存在 capacity 屬性户秤,而對于 Car 接口來說迹冤,它也不存在 capacity 屬性。那么虎忌,現(xiàn)在我們應(yīng)該如何解決以上問題呢泡徙?這時,我們可以使用類型守衛(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;
  }
}

在以上代碼中,我們使用 switchcase 運算符來實現(xiàn)類型守衛(wèi)挑围,從而確保在 evaluatePrice 方法中礁竞,我們可以安全地訪問 vehicle 對象中的所包含的屬性,來正確的計算該車輛類型所對應(yīng)的價格杉辙。

5.3 類型別名

類型別名用來給一個類型起個新名字模捂。

type Message = string | string[];

let greet = (message: Message) => {
  // ...
};

六、交叉類型

在 TypeScript 中交叉類型是將多個類型合并為一個類型蜘矢。通過 & 運算符可以將現(xiàn)有的多種類型疊加到一起成為一種類型狂男,它包含了所需的所有類型的特性。

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

let point: Point = {
  x: 1,
  y: 1
}

在上面代碼中我們先定義了 PartialPointX 類型品腹,接著使用 & 運算符創(chuàng)建一個新的 Point 類型岖食,表示一個含有 x 和 y 坐標的點,然后定義了一個 Point 類型的變量并初始化舞吭。

6.1 同名基礎(chǔ)類型屬性的合并

那么現(xiàn)在問題來了泡垃,假設(shè)在合并多個類型的過程中析珊,剛好出現(xiàn)某些類型存在相同的成員,但對應(yīng)的類型又不一致蔑穴,比如:

interface X {
  c: string;
  d: string;
}

interface Y {
  c: number;
  e: string
}

type XY = X & Y;
type YX = Y & X;

let p: XY;
let q: YX;

在上面的代碼中忠寻,接口 X 和接口 Y 都含有一個相同的成員 c,但它們的類型不一致存和。對于這種情況锡溯,此時 XY 類型或 YX 類型中成員 c 的類型是不是可以是 stringnumber 類型呢?比如下面的例子:

p = { c: 6, d: "d", e: "e" }; 
q = { c: "c", d: "d", e: "e" }; 

為什么接口 X 和接口 Y 混入后哑姚,成員 c 的類型會變成 never 呢祭饭?這是因為混入后成員 c 的類型為 string & number,即成員 c 的類型既可以是 string 類型又可以是 number 類型叙量。很明顯這種類型是不存在的倡蝙,所以混入后成員 c 的類型為 never

6.2 同名非基礎(chǔ)類型屬性的合并

在上面示例中绞佩,剛好接口 X 和接口 Y 中內(nèi)部成員 c 的類型都是基本數(shù)據(jù)類型寺鸥,那么如果是非基本數(shù)據(jù)類型的話,又會是什么情形品山。我們來看個具體的例子:

interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }

interface A { x: D; }
interface B { x: E; }
interface C { x: F; }

type ABC = A & B & C;

let abc: ABC = {
  x: {
    d: true,
    e: 'semlinker',
    f: 666
  }
};

console.log('abc:', abc);

以上代碼成功運行后胆建,控制臺會輸出以下結(jié)果:

由上圖可知,在混入多個類型時肘交,若存在相同的成員笆载,且成員類型為非基本數(shù)據(jù)類型,那么是可以成功合并涯呻。

七凉驻、TypeScript 函數(shù)

7.1 TypeScript 函數(shù)與 JavaScript 函數(shù)的區(qū)別

TypeScript JavaScript
含有類型 無類型
箭頭函數(shù) 箭頭函數(shù)(ES2015)
函數(shù)類型 無函數(shù)類型
必填和可選參數(shù) 所有參數(shù)都是可選的
默認參數(shù) 默認參數(shù)
剩余參數(shù) 剩余參數(shù)
函數(shù)重載 無函數(shù)重載

7.2 箭頭函數(shù)

1.常見語法
myBooks.forEach(() => console.log('reading'));

myBooks.forEach(title => console.log(title));

myBooks.forEach((title, idx, arr) =>
  console.log(idx + '-' + title);
);

myBooks.forEach((title, idx, arr) => {
  console.log(idx + '-' + title);
});
2.使用示例
// 未使用箭頭函數(shù)
function Book() {
  let self = this;
  self.publishDate = 2016;
  setInterval(function () {
    console.log(self.publishDate);
  }, 1000);
}

// 使用箭頭函數(shù)
function Book() {
  this.publishDate = 2016;
  setInterval(() => {
    console.log(this.publishDate);
  }, 1000);
}

7.3 參數(shù)類型和返回類型

function createUserId(name: string, id: number): string {
  return name + id;
}

7.4 函數(shù)類型

let IdGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
  return name + id;
}

IdGenerator = createUserId;

7.5 可選參數(shù)及默認參數(shù)

// 可選參數(shù)
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}

// 默認參數(shù)
function createUserId(
  name: string = "semlinker",
  id: number,
  age?: number
): string {
  return name + id;
}

在聲明函數(shù)時,可以通過 ? 號來定義可選參數(shù)复罐,比如 age?: number 這種形式涝登。在實際使用時,需要注意的是可選參數(shù)要放在普通參數(shù)的后面效诅,不然會導致編譯錯誤胀滚。

7.6 剩余參數(shù)

function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a = [];
push(a, 1, 2, 3);

7.7 函數(shù)重載

函數(shù)重載或方法重載是使用相同名稱和不同參數(shù)數(shù)量或類型創(chuàng)建多個方法的一種能力。

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
  // type Combinable = string | number;
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

在以上代碼中乱投,我們?yōu)?add 函數(shù)提供了多個函數(shù)類型定義咽笼,從而實現(xiàn)函數(shù)的重載。在 TypeScript 中除了可以重載普通函數(shù)之外篡腌,我們還可以重載類中的成員方法褐荷。

方法重載是指在同一個類中方法同名,參數(shù)不同(參數(shù)類型不同、參數(shù)個數(shù)不同或參數(shù)個數(shù)相同時參數(shù)的先后順序不同)解恰,調(diào)用時根據(jù)實參的形式隔盛,選擇與它匹配的方法執(zhí)行操作的一種技術(shù)珊泳。所以類中成員方法滿足重載的條件是:在同一個類中秘遏,方法名相同且參數(shù)列表不同传藏。下面我們來舉一個成員方法重載的例子:

class Calculator {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: string, b: number): string;
  add(a: number, b: string): string;
  add(a: Combinable, b: Combinable) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
    return a + b;
  }
}

const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');

這里需要注意的是乾忱,當 TypeScript 編譯器處理函數(shù)重載時限匣,它會查找重載列表抖苦,嘗試使用第一個重載定義。 如果匹配的話就使用這個米死。 因此锌历,在定義重載的時候,一定要把最精確的定義放在最前面峦筒。另外在 Calculator 類中究西,add(a: Combinable, b: Combinable){ } 并不是重載列表的一部分,因此對于 add 成員方法來說物喷,我們只定義了四個重載方法卤材。

八、TypeScript 數(shù)組

8.1 數(shù)組解構(gòu)

let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;

8.2 數(shù)組展開運算符

let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];

8.3 數(shù)組遍歷

let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
  console.log(i);
}

九峦失、TypeScript 對象

9.1 對象解構(gòu)

let person = {
  name: "Semlinker",
  gender: "Male",
};

let { name, gender } = person;

9.2 對象展開運算符

let person = {
  name: "Semlinker",
  gender: "Male",
  address: "Xiamen",
};

// 組裝對象
let personWithAge = { ...person, age: 33 };

// 獲取除了某些項外的其它項
let { name, ...rest } = person;

十扇丛、TypeScript 接口

在面向?qū)ο笳Z言中,接口是一個很重要的概念尉辑,它是對行為的抽象帆精,而具體如何行動需要由類去實現(xiàn)。

TypeScript 中的接口是一個非常靈活的概念隧魄,除了可用于對類的一部分行為進行抽象以外实幕,也常用于對「對象的形狀(Shape)」進行描述。

10.1 對象的形狀

interface Person {
  name: string;
  age: number;
}

let semlinker: Person = {
  name: "semlinker",
  age: 33,
};

10.2 可選 | 只讀屬性

interface Person {
  readonly name: string;
  age?: number;
}

只讀屬性用于限制只能在對象剛剛創(chuàng)建的時候修改其值堤器。此外 TypeScript 還提供了 ReadonlyArray<T> 類型昆庇,它與 Array<T> 相似,只是把所有可變方法去掉了闸溃,因此可以確保數(shù)組創(chuàng)建后再也不能被修改整吆。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

10.3 任意屬性

有時候我們希望一個接口中除了包含必選和可選屬性之外,還允許有其他的任意屬性辉川,這時我們可以使用 索引簽名 的形式來滿足上述要求表蝙。

interface Person {
  name: string;
  age?: number;
  [propName: string]: any;
}

const p1 = { name: "semlinker" };
const p2 = { name: "lolo", age: 5 };
const p3 = { name: "kakuqo", sex: 1 }

10.4 接口與類型別名的區(qū)別

1.Objects/Functions

接口和類型別名都可以用來描述對象的形狀或函數(shù)簽名:

接口

interface Point {
  x: number;
  y: number;
}

interface SetPoint {
  (x: number, y: number): void;
}

類型別名

type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;
2.Other Types

與接口類型不一樣,類型別名可以用于一些其他類型乓旗,比如原始類型府蛇、聯(lián)合類型和元組:

// primitive
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];
3.Extend

接口和類型別名都能夠被擴展,但語法有所不同屿愚。此外汇跨,接口和類型別名不是互斥的务荆。接口可以擴展類型別名,而反過來是不行的穷遂。

Interface extends interface

interface PartialPointX { x: number; }
interface Point extends PartialPointX { 
  y: number; 
}

Type alias extends type alias

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

Interface extends type alias

type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

Type alias extends interface

interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
4.Implements

類可以以相同的方式實現(xiàn)接口或類型別名函匕,但類不能實現(xiàn)使用類型別名定義的聯(lián)合類型:

interface Point {
  x: number;
  y: number;
}

class SomePoint implements Point {
  x = 1;
  y = 2;
}

type Point2 = {
  x: number;
  y: number;
};

class SomePoint2 implements Point2 {
  x = 1;
  y = 2;
}

type PartialPoint = { x: number; } | { y: number; };

// A class can only implement an object type or 
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
  x = 1;
  y = 2;
}
5.Declaration merging

與類型別名不同,接口可以定義多次蚪黑,會被自動合并為單個接口盅惜。

interface Point { x: number; }
interface Point { y: number; }

const point: Point = { x: 1, y: 2 };

十一、TypeScript 類

11.1 類的屬性與方法

在面向?qū)ο笳Z言中忌穿,類是一種面向?qū)ο笥嬎銠C編程語言的構(gòu)造抒寂,是創(chuàng)建對象的藍圖,描述了所創(chuàng)建的對象共同的屬性和方法掠剑。

在 TypeScript 中蓬推,我們可以通過 Class 關(guān)鍵字來定義一個類:

class Greeter {
  // 靜態(tài)屬性
  static cname: string = "Greeter";
  // 成員屬性
  greeting: string;

  // 構(gòu)造函數(shù) - 執(zhí)行初始化操作
  constructor(message: string) {
    this.greeting = message;
  }

  // 靜態(tài)方法
  static getClassName() {
    return "Class name is Greeter";
  }

  // 成員方法
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

那么成員屬性與靜態(tài)屬性,成員方法與靜態(tài)方法有什么區(qū)別呢澡腾?這里無需過多解釋沸伏,我們直接看一下編譯生成的 ES5 代碼:

"use strict";
var Greeter = /** @class */ (function () {
    // 構(gòu)造函數(shù) - 執(zhí)行初始化操作
    function Greeter(message) {
      this.greeting = message;
    }
    // 靜態(tài)方法
    Greeter.getClassName = function () {
      return "Class name is Greeter";
    };
    // 成員方法
    Greeter.prototype.greet = function () {
      return "Hello, " + this.greeting;
    };
    // 靜態(tài)屬性
    Greeter.cname = "Greeter";
    return Greeter;
}());
var greeter = new Greeter("world");

11.2 ECMAScript 私有字段

在 TypeScript 3.8 版本就開始支持ECMAScript 私有字段,使用方式如下:

class Person {
  #name: string;

  constructor(name: string) {
    this.#name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.#name}!`);
  }
}

let semlinker = new Person("Semlinker");

semlinker.#name;
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

與常規(guī)屬性(甚至使用 private 修飾符聲明的屬性)不同动分,私有字段要牢記以下規(guī)則:

  • 私有字段以 # 字符開頭毅糟,有時我們稱之為私有名稱;
  • 每個私有字段名稱都唯一地限定于其包含的類澜公;
  • 不能在私有字段上使用 TypeScript 可訪問性修飾符(如 public 或 private)姆另;
  • 私有字段不能在包含的類之外訪問,甚至不能被檢測到坟乾。

11.3 訪問器

在 TypeScript 中迹辐,我們可以通過 gettersetter 方法來實現(xiàn)數(shù)據(jù)的封裝和有效性校驗,防止出現(xiàn)異常數(shù)據(jù)甚侣。

let passcode = "Hello TypeScript";

class Employee {
  private _fullName: string;

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (passcode && passcode == "Hello TypeScript") {
      this._fullName = newName;
    } else {
      console.log("Error: Unauthorized update of employee!");
    }
  }
}

let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
  console.log(employee.fullName);
}

11.4 類的繼承

繼承(Inheritance)是一種聯(lián)結(jié)類與類的層次模型明吩。指的是一個類(稱為子類、子接口)繼承另外的一個類(稱為父類殷费、父接口)的功能印荔,并可以增加它自己的新功能的能力,繼承是類與類或者接口與接口之間最常見的關(guān)系详羡。

繼承是一種 is-a 關(guān)系:

image

在 TypeScript 中仍律,我們可以通過 extends 關(guān)鍵字來實現(xiàn)繼承:

class Animal {
  name: string;

  constructor(theName: string) {
    this.name = theName;
  }

  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Snake extends Animal {
  constructor(name: string) {
    super(name); // 調(diào)用父類的構(gòu)造函數(shù)
  }

  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

let sam = new Snake("Sammy the Python");
sam.move();

11.5 抽象類

使用 abstract 關(guān)鍵字聲明的類,我們稱之為抽象類实柠。抽象類不能被實例化水泉,因為它里面包含一個或多個抽象方法。所謂的抽象方法,是指不包含具體實現(xiàn)的方法:

abstract class Person {
  constructor(public name: string){}

  abstract say(words: string) :void;
}

// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error

抽象類不能被直接實例化草则,我們只能實例化實現(xiàn)了所有抽象方法的子類钢拧。具體如下所示:

abstract class Person {
  constructor(public name: string){}

  // 抽象方法
  abstract say(words: string) :void;
}

class Developer extends Person {
  constructor(name: string) {
    super(name);
  }

  say(words: string): void {
    console.log(`${this.name} says ${words}`);
  }
}

const lolo = new Developer("lolo");
lolo.say("I love ts!"); // lolo says I love ts!

11.6 類方法重載

在前面的章節(jié),我們已經(jīng)介紹了函數(shù)重載畔师。對于類的方法來說娶靡,它也支持重載牧牢。比如看锉,在以下示例中我們重載了 ProductService 類的 getProducts 成員方法:

class ProductService {
    getProducts(): void;
    getProducts(id: number): void;
    getProducts(id?: number) {
      if(typeof id === 'number') {
          console.log(`獲取id為 ${id} 的產(chǎn)品信息`);
      } else {
          console.log(`獲取所有的產(chǎn)品信息`);
      }  
    }
}

const productService = new ProductService();
productService.getProducts(666); // 獲取id為 666 的產(chǎn)品信息
productService.getProducts(); // 獲取所有的產(chǎn)品信息 

十二、TypeScript 泛型

軟件工程中塔鳍,我們不僅要創(chuàng)建一致的定義良好的 API伯铣,同時也要考慮可重用性。 組件不僅能夠支持當前的數(shù)據(jù)類型轮纫,同時也能支持未來的數(shù)據(jù)類型腔寡,這在創(chuàng)建大型系統(tǒng)時為你提供了十分靈活的功能。

在像 C# 和 Java 這樣的語言中掌唾,可以使用泛型來創(chuàng)建可重用的組件放前,一個組件可以支持多種類型的數(shù)據(jù)。 這樣用戶就可以以自己的數(shù)據(jù)類型來使用組件糯彬。

設(shè)計泛型的關(guān)鍵目的是在成員之間提供有意義的約束凭语,這些成員可以是:類的實例成員、類的方法撩扒、函數(shù)參數(shù)和函數(shù)返回值似扔。

泛型(Generics)是允許同一個函數(shù)接受不同類型參數(shù)的一種模板。相比于使用 any 類型搓谆,使用泛型來創(chuàng)建可復用的組件要更好炒辉,因為泛型會保留參數(shù)類型。

12.1 泛型語法

對于剛接觸 TypeScript 泛型的讀者來說泉手,首次看到 <T> 語法會感到陌生黔寇。其實它沒有什么特別,就像傳遞參數(shù)一樣斩萌,我們傳遞了我們想要用于特定函數(shù)調(diào)用的類型啡氢。

image

參考上面的圖片,當我們調(diào)用 identity<Number>(1) 术裸,Number 類型就像參數(shù) 1 一樣倘是,它將在出現(xiàn) T 的任何位置填充該類型。圖中 <T> 內(nèi)部的 T 被稱為類型變量袭艺,它是我們希望傳遞給 identity 函數(shù)的類型占位符搀崭,同時它被分配給 value 參數(shù)用來代替它的類型:此時 T 充當?shù)氖穷愋停皇翘囟ǖ?Number 類型。

其中 T 代表 Type瘤睹,在定義泛型時通常用作第一個類型變量名稱升敲。但實際上 T 可以用任何有效名稱代替。除了 T 之外轰传,以下是常見泛型變量代表的意思:

  • K(Key):表示對象中的鍵類型驴党;
  • V(Value):表示對象中的值類型;
  • E(Element):表示元素類型获茬。

其實并不是只能定義一個類型變量港庄,我們可以引入希望定義的任何數(shù)量的類型變量。比如我們引入一個新的類型變量 U恕曲,用于擴展我們定義的 identity 函數(shù):

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

console.log(identity<Number, string>(68, "Semlinker"));
image

除了為類型變量顯式設(shè)定值之外鹏氧,一種更常見的做法是使編譯器自動選擇這些類型,從而使代碼更簡潔佩谣。我們可以完全省略尖括號把还,比如:

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

console.log(identity(68, "Semlinker"));

對于上述代碼,編譯器足夠聰明茸俭,能夠知道我們的參數(shù)類型吊履,并將它們賦值給 T 和 U,而不需要開發(fā)人員顯式指定它們调鬓。

12.2 泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}

12.3 泛型類

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

12.4 泛型工具類型

為了方便開發(fā)者 TypeScript 內(nèi)置了一些常用的工具類型艇炎,比如 Partial、Required袖迎、Readonly冕臭、Record 和 ReturnType 等。出于篇幅考慮燕锥,這里我們只簡單介紹 Partial 工具類型辜贵。不過在具體介紹之前,我們得先介紹一些相關(guān)的基礎(chǔ)知識归形,方便讀者自行學習其它的工具類型托慨。

1.typeof

在 TypeScript 中,typeof 操作符可以用來獲取一個變量聲明或?qū)ο蟮念愋汀?/p>

interface Person {
  name: string;
  age: number;
}

const sem: Person = { name: 'semlinker', age: 33 };
type Sem= typeof sem; // -> Person

function toArray(x: number): Array<number> {
  return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]
2.keyof

keyof 操作符是在 TypeScript 2.1 版本引入的暇榴,該操作符可以用于獲取某種類型的所有鍵厚棵,其返回類型是聯(lián)合類型。

interface Person {
  name: string;
  age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number

在 TypeScript 中支持兩種索引簽名蔼紧,數(shù)字索引和字符串索引:

interface StringArray {
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string; 
}

interface StringArray1 {
  // 數(shù)字索引 -> keyof StringArray1 => number
  [index: number]: string;
}

為了同時支持兩種索引類型婆硬,就得要求數(shù)字索引的返回值必須是字符串索引返回值的子類。其中的原因就是當使用數(shù)值索引時奸例,JavaScript 在執(zhí)行索引操作時彬犯,會先把數(shù)值索引先轉(zhuǎn)換為字符串索引向楼。所以 keyof { [x: string]: Person } 的結(jié)果會返回 string | number

3.in

in 用來遍歷枚舉類型:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }
4.infer

在條件類型語句中谐区,可以用 infer 聲明一個類型變量并且對它進行使用湖蜕。

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

以上代碼中 infer R 就是聲明一個變量來承載傳入函數(shù)簽名的返回值類型,簡單說就是用它取到函數(shù)返回值的類型方便之后使用宋列。

5.extends

有時候我們定義的泛型不想過于靈活或者說想繼承某些類等昭抒,可以通過 extends 關(guān)鍵字添加泛型約束。

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

現(xiàn)在這個泛型函數(shù)被定義了約束炼杖,因此它不再是適用于任意類型:

loggingIdentity(3);  // Error, number doesn't have a .length property

這時我們需要傳入符合約束類型的值灭返,必須包含必須的屬性:

loggingIdentity({length: 10, value: 3});
6.Partial

Partial<T> 的作用就是將某個類型里的屬性全部變?yōu)榭蛇x項 ?

定義:

/**
 * node_modules/typescript/lib/lib.es5.d.ts
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

在以上代碼中嘹叫,首先通過 keyof T 拿到 T 的所有屬性名婆殿,然后使用 in 進行遍歷诈乒,將值賦給 P罩扇,最后通過 T[P] 取得相應(yīng)的屬性值。中間的 ? 號怕磨,用于將所有屬性變?yōu)榭蛇x喂饥。

示例:

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
  title: "Learn TS",
  description: "Learn TypeScript",
};

const todo2 = updateTodo(todo1, {
  description: "Learn TypeScript Enum",
});

在上面的 updateTodo 方法中,我們利用 Partial<T> 工具類型肠鲫,定義 fieldsToUpdate 的類型為 Partial<Todo>员帮,即:

{
   title?: string | undefined;
   description?: string | undefined;
}

十三、TypeScript 裝飾器

13.1 裝飾器是什么

  • 它是一個表達式
  • 該表達式被執(zhí)行后导饲,返回一個函數(shù)
  • 函數(shù)的入?yún)⒎謩e為 target捞高、name 和 descriptor
  • 執(zhí)行該函數(shù)后,可能返回 descriptor 對象渣锦,用于配置 target 對象

13.2 裝飾器的分類

  • 類裝飾器(Class decorators)
  • 屬性裝飾器(Property decorators)
  • 方法裝飾器(Method decorators)
  • 參數(shù)裝飾器(Parameter decorators)

需要注意的是硝岗,若要啟用實驗性的裝飾器特性,你必須在命令行或 tsconfig.json 里啟用 experimentalDecorators 編譯器選項:

命令行

tsc --target ES5 --experimentalDecorators

tsconfig.json

{
  "compilerOptions": {
     "target": "ES5",
     "experimentalDecorators": true
   }
}

13.3 類裝飾器

類裝飾器聲明:

declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

類裝飾器顧名思義袋毙,就是用來裝飾類的型檀。它接收一個參數(shù):

  • target: TFunction - 被裝飾的類

看完第一眼后,是不是感覺都不好了听盖。沒事胀溺,我們馬上來個例子:

function Greeter(target: Function): void {
  target.prototype.greet = function (): void {
    console.log("Hello Semlinker!");
  };
}

@Greeter
class Greeting {
  constructor() {
    // 內(nèi)部實現(xiàn)
  }
}

let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello Semlinker!';

上面的例子中,我們定義了 Greeter 類裝飾器皆看,同時我們使用了 @Greeter 語法糖仓坞,來使用裝飾器。

友情提示:讀者可以直接復制上面的代碼腰吟,在 TypeScript Playground 中運行查看結(jié)果无埃。

有的讀者可能想問,例子中總是輸出 Hello Semlinker! ,能自定義輸出的問候語么 录语?這個問題很好倍啥,答案是可以的。

具體實現(xiàn)如下:

function Greeter(greeting: string) {
  return function (target: Function) {
    target.prototype.greet = function (): void {
      console.log(greeting);
    };
  };
}

@Greeter("Hello TS!")
class Greeting {
  constructor() {
    // 內(nèi)部實現(xiàn)
  }
}

let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello TS!';

13.4 屬性裝飾器

屬性裝飾器聲明:

declare type PropertyDecorator = (target:Object, 
  propertyKey: string | symbol ) => void;

屬性裝飾器顧名思義澎埠,用來裝飾類的屬性虽缕。它接收兩個參數(shù):

  • target: Object - 被裝飾的類
  • propertyKey: string | symbol - 被裝飾類的屬性名

趁熱打鐵,馬上來個例子熱熱身:

function logProperty(target: any, key: string) {
  delete target[key];

  const backingField = "_" + key;

  Object.defineProperty(target, backingField, {
    writable: true,
    enumerable: true,
    configurable: true
  });

  // property getter
  const getter = function (this: any) {
    const currVal = this[backingField];
    console.log(`Get: ${key} => ${currVal}`);
    return currVal;
  };

  // property setter
  const setter = function (this: any, newVal: any) {
    console.log(`Set: ${key} => ${newVal}`);
    this[backingField] = newVal;
  };

  // Create new property with getter and setter
  Object.defineProperty(target, key, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Person { 
  @logProperty
  public name: string;

  constructor(name : string) { 
    this.name = name;
  }
}

const p1 = new Person("semlinker");
p1.name = "kakuqo";

以上代碼我們定義了一個 logProperty 函數(shù)蒲稳,來跟蹤用戶對屬性的操作氮趋,當代碼成功運行后,在控制臺會輸出以下結(jié)果:

Set: name => semlinker
Set: name => kakuqo

13.5 方法裝飾器

方法裝飾器聲明:

declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,         
  descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

方法裝飾器顧名思義江耀,用來裝飾類的方法剩胁。它接收三個參數(shù):

  • target: Object - 被裝飾的類
  • propertyKey: string | symbol - 方法名
  • descriptor: TypePropertyDescript - 屬性描述符

廢話不多說,直接上例子:

function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  let originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log("wrapped function: before invoking " + propertyKey);
    let result = originalMethod.apply(this, args);
    console.log("wrapped function: after invoking " + propertyKey);
    return result;
  };
}

class Task {
  @log
  runTask(arg: any): any {
    console.log("runTask invoked, args: " + arg);
    return "finished";
  }
}

let task = new Task();
let result = task.runTask("learn ts");
console.log("result: " + result);

以上代碼成功運行后祥国,控制臺會輸出以下結(jié)果:

"wrapped function: before invoking runTask" 
"runTask invoked, args: learn ts" 
"wrapped function: after invoking runTask" 
"result: finished" 

下面我們來介紹一下參數(shù)裝飾器昵观。

13.6 參數(shù)裝飾器

參數(shù)裝飾器聲明:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, 
  parameterIndex: number ) => void

參數(shù)裝飾器顧名思義,是用來裝飾函數(shù)參數(shù)舌稀,它接收三個參數(shù):

  • target: Object - 被裝飾的類
  • propertyKey: string | symbol - 方法名
  • parameterIndex: number - 方法中參數(shù)的索引值
function Log(target: Function, key: string, parameterIndex: number) {
  let functionLogged = key || target.prototype.constructor.name;
  console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
    been decorated`);
}

class Greeter {
  greeting: string;
  constructor(@Log phrase: string) {
    this.greeting = phrase; 
  }
}

以上代碼成功運行后啊犬,控制臺會輸出以下結(jié)果:

"The parameter in position 0 at Greeter has been decorated" 

十四、TypeScript 4.0 新特性

TypeScript 4.0 帶來了很多新的特性壁查,這里我們只簡單介紹其中的兩個新特性觉至。

14.1 構(gòu)造函數(shù)的類屬性推斷

noImplicitAny 配置屬性被啟用之后,TypeScript 4.0 就可以使用控制流分析來確認類中的屬性類型:

class Person {
  fullName; // (property) Person.fullName: string
  firstName; // (property) Person.firstName: string
  lastName; // (property) Person.lastName: string

  constructor(fullName: string) {
    this.fullName = fullName;
    this.firstName = fullName.split(" ")[0];
    this.lastName =   fullName.split(" ")[1];
  }  
}

然而對于以上的代碼睡腿,如果在 TypeScript 4.0 以前的版本语御,比如在 3.9.2 版本下,編譯器會提示以下錯誤信息:

class Person {
  // Member 'fullName' implicitly has an 'any' type.(7008)
  fullName; // Error
  firstName; // Error
  lastName; // Error

  constructor(fullName: string) {
    this.fullName = fullName;
    this.firstName = fullName.split(" ")[0];
    this.lastName =   fullName.split(" ")[1];
  }  
}

從構(gòu)造函數(shù)推斷類屬性的類型席怪,該特性給我們帶來了便利应闯。但在使用過程中,如果我們沒法保證對成員屬性都進行賦值何恶,那么該屬性可能會被認為是 undefined孽锥。

class Person {
   fullName;  // (property) Person.fullName: string
   firstName; // (property) Person.firstName: string | undefined
   lastName; // (property) Person.lastName: string | undefined

   constructor(fullName: string) {
     this.fullName = fullName;
     if(Math.random()){
       this.firstName = fullName.split(" ")[0];
       this.lastName =   fullName.split(" ")[1];
     }
   }  
}

14.2 標記的元組元素

在以下的示例中,我們使用元組類型來聲明剩余參數(shù)的類型:

function addPerson(...args: [string, number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
}

addPerson("lolo", 5); // Person info: name: lolo, age: 5 

其實细层,對于上面的 addPerson 函數(shù)惜辑,我們也可以這樣實現(xiàn):

function addPerson(name: string, age: number) {
  console.log(`Person info: name: ${name}, age: ${age}`)
}

這兩種方式看起來沒有多大的區(qū)別,但對于第一種方式疫赎,我們沒法設(shè)置第一個參數(shù)和第二個參數(shù)的名稱盛撑。雖然這樣對類型檢查沒有影響,但在元組位置上缺少標簽捧搞,會使得它們難于使用抵卫。為了提高開發(fā)者使用元組的體驗狮荔,TypeScript 4.0 支持為元組類型設(shè)置標簽:

function addPerson(...args: [name: string, age: number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}

之后,當我們使用 addPerson 方法時介粘,TypeScript 的智能提示就會變得更加友好殖氏。

// 未使用標簽的智能提示
// addPerson(args_0: string, args_1: number): void
function addPerson(...args: [string, number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
} 

// 已使用標簽的智能提示
// addPerson(name: string, age: number): void
function addPerson(...args: [name: string, age: number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
} 

十五、編譯上下文

15.1 tsconfig.json 的作用

  • 用于標識 TypeScript 項目的根路徑姻采;
  • 用于配置 TypeScript 編譯器雅采;
  • 用于指定編譯的文件。

15.2 tsconfig.json 重要字段

  • files - 設(shè)置要編譯的文件的名稱慨亲;
  • include - 設(shè)置需要進行編譯的文件婚瓜,支持路徑模式匹配;
  • exclude - 設(shè)置無需進行編譯的文件刑棵,支持路徑模式匹配巴刻;
  • compilerOptions - 設(shè)置與編譯流程相關(guān)的選項。

15.3 compilerOptions 選項

compilerOptions 支持很多選項蛉签,常見的有 baseUrl胡陪、 targetbaseUrl正蛙、 moduleResolutionlib 等督弓。

compilerOptions 每個選項的詳細說明如下:

{
  "compilerOptions": {

    /* 基本選項 */
    "target": "es5",                       // 指定 ECMAScript 目標版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模塊: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在編譯中的庫文件
    "allowJs": true,                       // 允許編譯 javascript 文件
    "checkJs": true,                       // 報告 javascript 文件中的錯誤
    "jsx": "preserve",                     // 指定 jsx 代碼的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相應(yīng)的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相應(yīng)的 '.map' 文件
    "outFile": "./",                       // 將輸出文件合并為一個文件
    "outDir": "./",                        // 指定輸出目錄
    "rootDir": "./",                       // 用來控制輸出目錄結(jié)構(gòu) --outDir.
    "removeComments": true,                // 刪除編譯后的所有的注釋
    "noEmit": true,                        // 不生成輸出文件
    "importHelpers": true,                 // 從 tslib 導入輔助工具函數(shù)
    "isolatedModules": true,               // 將每個文件做為單獨的模塊 (與 'ts.transpileModule' 類似).

    /* 嚴格的類型檢查選項 */
    "strict": true,                        // 啟用所有嚴格類型檢查選項
    "noImplicitAny": true,                 // 在表達式和聲明上有隱含的 any類型時報錯
    "strictNullChecks": true,              // 啟用嚴格的 null 檢查
    "noImplicitThis": true,                // 當 this 表達式值為 any 類型的時候营曼,生成一個錯誤
    "alwaysStrict": true,                  // 以嚴格模式檢查每個模塊乒验,并在每個文件里加入 'use strict'

    /* 額外的檢查 */
    "noUnusedLocals": true,                // 有未使用的變量時,拋出錯誤
    "noUnusedParameters": true,            // 有未使用的參數(shù)時蒂阱,拋出錯誤
    "noImplicitReturns": true,             // 并不是所有函數(shù)里的代碼都有返回值時锻全,拋出錯誤
    "noFallthroughCasesInSwitch": true,    // 報告 switch 語句的 fallthrough 錯誤。(即录煤,不允許 switch 的 case 語句貫穿)

    /* 模塊解析選項 */
    "moduleResolution": "node",            // 選擇模塊解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相對模塊名稱的基目錄
    "paths": {},                           // 模塊名到基于 baseUrl 的路徑映射的列表
    "rootDirs": [],                        // 根文件夾列表鳄厌,其組合內(nèi)容表示項目運行時的結(jié)構(gòu)內(nèi)容
    "typeRoots": [],                       // 包含類型聲明的文件列表
    "types": [],                           // 需要包含的類型聲明文件名列表
    "allowSyntheticDefaultImports": true,  // 允許從沒有設(shè)置默認導出的模塊中默認導入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定調(diào)試器應(yīng)該找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定調(diào)試器應(yīng)該找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成單個 soucemaps 文件妈踊,而不是將 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 將代碼與 sourcemaps 生成到一個文件中了嚎,要求同時設(shè)置了 --inlineSourceMap 或 --sourceMap 屬性

    /* 其他選項 */
    "experimentalDecorators": true,        // 啟用裝飾器
    "emitDecoratorMetadata": true          // 為裝飾器提供元數(shù)據(jù)的支持
  }
}

十六、TypeScript 開發(fā)輔助工具

16.1 TypeScript Playground

簡介:TypeScript 官方提供的在線 TypeScript 運行環(huán)境廊营,利用它你可以方便地學習 TypeScript 相關(guān)知識與不同版本的功能特性歪泳。

在線地址:https://www.typescriptlang.org/play/

除了 TypeScript 官方的 Playground 之外,你還可以選擇其他的 Playground露筒,比如 codepen.io呐伞、stackblitzjsbin.com 等。

16.2 TypeScript UML Playground

簡介:一款在線 TypeScript UML 工具慎式,利用它你可以為指定的 TypeScript 代碼生成 UML 類圖伶氢。

在線地址:https://tsuml-demo.firebaseapp.com/

16.3 JSON TO TS

簡介:一款 TypeScript 在線工具趟径,利用它你可以為指定的 JSON 數(shù)據(jù)生成對應(yīng)的 TypeScript 接口定義。

在線地址:http://www.jsontots.com/

除了使用 jsontots 在線工具之外癣防,對于使用 VSCode IDE 的小伙們還可以安裝 JSON to TS 擴展來快速完成 JSON to TS 的轉(zhuǎn)換工作蜗巧。

16.4 Schemats

簡介:利用 Schemats,你可以基于(Postgres蕾盯,MySQL)SQL 數(shù)據(jù)庫中的 schema 自動生成 TypeScript 接口定義惧蛹。

在線地址:https://github.com/SweetIQ/schemats

16.5 TypeScript AST Viewer

簡介:一款 TypeScript AST 在線工具,利用它你可以查看指定 TypeScript 代碼對應(yīng)的 AST(Abstract Syntax Tree)抽象語法樹刑枝。

在線地址:https://ts-ast-viewer.com/

對于了解過 AST 的小伙伴來說香嗓,對 astexplorer 這款在線工具應(yīng)該不會陌生。該工具除了支持 JavaScript 之外装畅,還支持 CSS靠娱、JSON、RegExp掠兄、GraphQL 和 Markdown 等格式的解析像云。

16.6 TypeDoc

簡介:TypeDoc 用于將 TypeScript 源代碼中的注釋轉(zhuǎn)換為 HTML 文檔或 JSON 模型。它可靈活擴展蚂夕,并支持多種配置迅诬。

在線地址:https://typedoc.org/

16.7 TypeScript ESLint

簡介:使用 TypeScript ESLint 可以幫助我們規(guī)范代碼質(zhì)量,提高團隊開發(fā)效率婿牍。

在線地址:https://typescript-eslint.io/

TypeScript ESLint 項目感興趣且想在項目中應(yīng)用的小伙伴侈贷,可以參考 “在Typescript項目中,如何優(yōu)雅的使用ESLint和Prettier” 這篇文章等脂。

能堅持看到這里的小伙伴都是 “真愛”俏蛮,如果你還意猶未盡,那就來看看本人整理的 Github 上 1.8K+ 的開源項目:awesome-typescript上遥。

十七搏屑、參考資源

轉(zhuǎn)自:https://juejin.im/post/6872111128135073806

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市粉楚,隨后出現(xiàn)的幾起案子辣恋,更是在濱河造成了極大的恐慌,老刑警劉巖模软,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伟骨,死亡現(xiàn)場離奇詭異,居然都是意外死亡撵摆,警方通過查閱死者的電腦和手機底靠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來特铝,“玉大人暑中,你說我怎么就攤上這事膝但》姘耍” “怎么了枕面?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵励翼,是天一觀的道長。 經(jīng)常有香客問我雕凹,道長殴俱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任枚抵,我火速辦了婚禮线欲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘汽摹。我一直安慰自己李丰,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布逼泣。 她就那樣靜靜地躺著趴泌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拉庶。 梳的紋絲不亂的頭發(fā)上嗜憔,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音氏仗,去河邊找鬼吉捶。 笑死,一個胖子當著我的面吹牛廓鞠,可吹牛的內(nèi)容都是我干的帚稠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼床佳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了榄审?” 一聲冷哼從身側(cè)響起砌们,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎搁进,沒想到半個月后浪感,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡饼问,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年影兽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莱革。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡峻堰,死狀恐怖讹开,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捐名,我是刑警寧澤旦万,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站镶蹋,受9級特大地震影響成艘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贺归,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一淆两、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拂酣,春花似錦琼腔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至尸诽,卻和暖如春甥材,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背性含。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工洲赵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人商蕴。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓叠萍,卻偏偏與公主長得像,于是被迫代替她去往敵國和親绪商。 傳聞我的和親對象是個殘疾皇子苛谷,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容