想學習 TypeScript 的小伙伴看過來,本文將帶你一步步學習 TypeScript 入門相關的十四個知識點,詳細的內容大綱請看下圖: 一、TypeScript 是什麼 TypeScript 是一種由微軟開發的自由和開源的編程語言。它是 JavaScript 的一個超集,而且本質上向這個語言添加 ...
一、TypeScript 是什麼
TypeScript 是一種由微軟開發的自由和開源的編程語言。它是 JavaScript 的一個超集,而且本質上向這個語言添加了可選的靜態類型和基於類的面向對象編程。
TypeScript 提供最新的和不斷發展的 JavaScript 特性,包括那些來自 2015 年的 ECMAScript 和未來的提案中的特性,比如非同步功能和 Decorators,以幫助建立健壯的組件。下圖顯示了 TypeScript 與 ES5、ES2015 和 ES2016 之間的關係:
1.1 TypeScript 與 JavaScript 的區別
1.2 獲取 TypeScript
命令行的 TypeScript 編譯器可以使用 Node.js 包來安裝。
1.安裝 TypeScript
$ npm install -g typescript
2.編譯 TypeScript 文件
$ tsc helloworld.ts# helloworld.ts => helloworld.js
當然,對於剛入門 TypeScript 的小伙伴,也可以不用安裝 typescript
,而是直接使用線上的 TypeScript Playground 來學習新的語法或新特性。
二、TypeScript 基礎類型
2.1 Boolean 類型
let isDone: boolean = false;// ES5:var isDone = false;
2.2 Number 類型
let count: number = 10;// ES5:var count = 10;
String 類型
let name: string = "Semliker";// ES5:var name = 'Semlinker';
2.4 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.5 Enum 類型
使用枚舉我們可以定義一些帶名字的常量。 使用枚舉可以清晰地表達意圖或創建一組有區別的用例。 TypeScript 支持數字的和基於字元串的枚舉。
1.數字枚舉
enum Direction { NORTH, SOUTH, EAST, WEST,}let dir: Direction = Direction.NORTH;
預設情況下,NORTH 的初始值為 0,其餘的成員會從 1 開始自動增長。換句話說,Direction.SOUTH 的值為 1,Direction.EAST 的值為 2,Direction.WEST 的值為 3。上面的枚舉示例代碼經過編譯後會生成以下代碼:
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
當然我們也可以設置 NORTH 的初始值,比如:
enum Direction { NORTH = 3, SOUTH, EAST, WEST,}
2.字元串枚舉
在 TypeScript 2.4 版本,允許我們使用字元串枚舉。在一個字元串枚舉里,每個成員都必須用字元串字面量,或另外一個字元串枚舉成員進行初始化。
enum Direction { NORTH = "NORTH", SOUTH = "SOUTH", EAST = "EAST", WEST = "WEST",}
以上代碼對於的 ES5 代碼如下:
"use strict";
var Direction;
(function (Direction) {
Direction["NORTH"] = "NORTH";
Direction["SOUTH"] = "SOUTH";
Direction["EAST"] = "EAST";
Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));
3.異構枚舉
異構枚舉的成員值是數字和字元串的混合:
enum Enum { A, B, C = "C", D = "D", E = 8, F,}
以上代碼對於的 ES5 代碼如下:
enum Enum {
A,
B,
C = "C",
D = "D",
E = 8,
F,
}
通過觀察上述生成的 ES5 代碼,我們可以發現數字枚舉相對字元串枚舉多了 “反向映射”:
console.log(Enum.A) //輸出:0console.log(Enum[0]) // 輸出:A
2.6 Any 類型
在 TypeScript 中,任何類型都可以被歸為 any 類型。這讓 any 類型成為了類型系統的頂級類型(也被稱作全局超級類型)。
let notSure: any = 666;notSure = "Semlinker";notSure = false;
any
類型本質上是類型系統的一個逃逸艙。作為開發者,這給了我們很大的自由:TypeScript 允許我們對 any
類型的值執行任何操作,而無需事先執行任何形式的檢查。比如:
let value: any;value.foo.bar; // OKvalue.trim(); // OKvalue(); // OKnew value(); // OKvalue[0][1]; // OK
在許多場景下,這太寬鬆了。使用 any
類型,可以很容易地編寫類型正確但在運行時有問題的代碼。如果我們使用 any
類型,就無法使用 TypeScript 提供的大量的保護機制。為瞭解決 any
帶來的問題,TypeScript 3.0 引入了 unknown
類型。
2.7 Unknown 類型
就像所有類型都可以賦值給 any
,所有類型也都可以賦值給 unknown
。這使得 unknown
成為 TypeScript 類型系統的另一種頂級類型(另一種是 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
的值賦值給其他類型的變數時會發生什麼?
let value: unknown;let value1: unknown = value; // OKlet value2: any = value; // OKlet value3: boolean = value; // Errorlet value4: number = value; // Errorlet value5: string = value; // Errorlet value6: object = value; // Errorlet value7: any[] = value; // Errorlet value8: Function = value; // Error
unknown
類型只能被賦值給 any
類型和 unknown
類型本身。直觀地說,這是有道理的:只有能夠保存任意類型值的容器才能保存 unknown
類型的值。畢竟我們不知道變數 value
中存儲了什麼類型的值。
現在讓我們看看當我們嘗試對類型為 unknown
的值執行操作時會發生什麼。以下是我們在之前 any
章節看過的相同操作:
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
將 value
變數類型設置為 unknown
後,這些操作都不再被認為是類型正確的。通過將 any
類型改變為 unknown
類型,我們已將允許所有更改的預設設置,更改為禁止任何更改。
2.8 Tuple 類型
眾所周知,數組一般由同種類型的值組成,但有時我們需要在單個變數中存儲不同類型的值,這時候我們就可以使用元組。在 JavaScript 中是沒有元組的,元組是 TypeScript 中特有的類型,其工作方式類似於數組。
元組可用於定義具有有限數量的未命名屬性的類型。每個屬性都有一個關聯的類型。使用元組時,必須提供每個屬性的值。為了更直觀地理解元組的概念,我們來看一個具體的例子:
let tupleType: [string, boolean];tupleType = ["Semlinker", true];
在上面代碼中,我們定義了一個名為 tupleType
的變數,它的類型是一個類型數組 [string, boolean]
,然後我們按照正確的類型依次初始化 tupleType 變數。與數組一樣,我們可以通過下標來訪問元組中的元素:
console.log(tupleType[0]); // Semlinkerconsole.log(tupleType[1]); // true
在元組初始化的時候,如果出現類型不匹配的話,比如:
tupleType = [true, "Semlinker"];
此時,TypeScript 編譯器會提示以下錯誤信息:
[0]: Type 'true' is not assignable to type 'string'.[1]: Type 'string' is not assignable to type 'boolean'.
很明顯是因為類型不匹配導致的。在元組初始化的時候,我們還必須提供每個屬性的值,不然也會出現錯誤,比如:
tupleType = ["Semlinker"];
此時,TypeScript 編譯器會提示以下錯誤信息:
Property '1' is missing in type '[string]' but required in type '[string, boolean]'.
2.9 Void 類型
某種程度上來說,void 類型像是與 any 類型相反,它表示沒有任何類型。當一個函數沒有返回值時,你通常會見到其返回值類型是 void:
// 聲明函數返回值為voidfunction warnUser(): void { console.log("This is my warning message");}
以上代碼編譯生成的 ES5 代碼如下:
"use strict";function warnUser() { console.log("This is my warning message");}
需要註意的是,聲明一個 void 類型的變數沒有什麼作用,因為它的值只能為 undefined
或 null
:
let unusable: void = undefined;
2.10 Null 和 Undefined 類型
TypeScript 里,undefined
和 null
兩者有各自的類型分別為 undefined
和 null
。
let u: undefined = undefined;let n: null = null;
預設情況下 null
和 undefined
是所有類型的子類型。 就是說你可以把 null
和 undefined
賦值給 number
類型的變數。然而,如果你指定了--strictNullChecks
標記,null
和 undefined
只能賦值給 void
和它們各自的類型。
2.11 Never 類型
never
類型表示的是那些永不存在的值的類型。 例如,never
類型是那些總是會拋出異常或根本就不會有返回值的函數表達式或箭頭函數表達式的返回值類型。
// 返回never的函數必須存在無法達到的終點
function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
在 TypeScript 中,可以利用 never 類型的特性來實現全面性檢查,具體示例如下:
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 變數。如果一切邏輯正確,那麼這裡應該能夠編譯通過。但是假如後來有一天你的同事修改了 Foo 的類型:
type Foo = string | number | boolean;
然而他忘記同時修改 controlFlowAnalysisWithNever
方法中的控制流程,這時候 else 分支的 foo 類型會被收窄為 boolean
類型,導致無法賦值給 never 類型,這時就會產生一個編譯錯誤。通過這個方式,我們可以確保
controlFlowAnalysisWithNever
方法總是窮盡了 Foo 的所有可能類型。 通過這個示例,我們可以得出一個結論:使用 never 避免出現新增了聯合類型沒有對應的實現,目的就是寫出類型絕對安全的代碼。
三、TypeScript 斷言
有時候你會遇到這樣的情況,你會比 TypeScript 更瞭解某個值的詳細信息。通常這會發生在你清楚地知道一個實體具有比它現有類型更確切的類型。
通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在乾什麼”。類型斷言好比其他語言里的類型轉換,但是不進行特殊的數據檢查和解構。它沒有運行時的影響,只是在編譯階段起作用。
類型斷言有兩種形式:
3.1 “尖括弧” 語法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
3.2 as 語法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
四、類型守衛
A type guard is some expression that performs a runtime check that guarantees the type in some scope. —— TypeScript 官方文檔
類型保護是可執行運行時檢查的一種表達式,用於確保該類型在一定的範圍內。換句話說,類型保護可以保證一個字元串是一個字元串,儘管它的值也可以是一個數值。類型保護與特性檢測並不是完全不同,其主要思想是嘗試檢測屬性、方法或原型,以確定如何處理值。目前主要有四種的方式來實現類型保護:
4.1 in 關鍵字
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 關鍵字
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 關鍵字
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";
}
五、聯合類型和類型別名
5.1 聯合類型
聯合類型通常與 null
或 undefined
一起使用:
const sayHello = (name: string | undefined) => { /* ... */};
例如,這裡 name
的類型是 string | undefined
意味著可以將 string
或 undefined
的值傳遞給sayHello
函數。
sayHello("Semlinker");sayHello(undefined);
通過這個示例,你可以憑直覺知道類型 A 和類型 B 聯合後的類型是同時接受 A 和 B 值的類型。
5.2 可辨識聯合
TypeScript 可辨識聯合(Discriminated Unions)類型,也稱為代數數據類型或標簽聯合類型。它包含 3 個要點:可辨識、聯合類型和類型守衛。
這種類型的本質是結合聯合類型和字面量類型的一種類型保護方法。如果一個類型是多個類型的聯合類型,且多個類型含有一個公共屬性,那麼就可以利用這個公共屬性,來創建不同的類型保護區塊。
1.可辨識
可辨識要求聯合類型中的每個元素都含有一個單例類型屬性,比如:
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
三個介面,在這些介面中都包含一個 vType
屬性,該屬性被稱為可辨識的屬性,而其它的屬性只跟特性的介面相關。
2.聯合類型
基於前面定義了三個介面,我們可以創建一個 Vehicle
聯合類型:
type Vehicle = Motorcycle | Car | Truck;
現在我們就可以開始使用 Vehicle
聯合類型,對於 Vehicle
類型的變數,它可以表示不同類型的車輛。
3.類型守衛
下麵我們來定義一個 evaluatePrice
方法,該方法用於根據車輛的類型、容量和評估因數來計算價格,具體實現如下:
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
屬性。那麼,現在我們應該如何解決以上問題呢?這時,我們可以使用類型守衛。下麵我們來重構一下前面定義的 evaluatePrice
方法,重構後的代碼如下:
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
運算符來實現類型守衛,從而確保在 evaluatePrice
方法中,我們可以安全地訪問 vehicle
對象中的所包含的屬性,來正確的計算該車輛類型所對應的價格。
5.3 類型別名
類型別名用來給一個類型起個新名字。
type Message = string | string[];let greet = (message: Message) => { // ...};