想學習 TypeScript 的小伙伴看過來,本文將帶你一步步學習 TypeScript 入門相關的十四個知識點,詳細的內容大綱請看下圖:

一、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) => { // ...};
六、交叉型別
TypeScript 交叉型別是將多個型別合并為一個型別, 這讓我們可以把現有的多種型別疊加到一起成為一種型別,它包含了所需的所有型別的特性,
?
interface IPerson {
id: string;
age: number;
}
?
interface IWorker {
companyId: string;
}
?
type IStaff = IPerson & IWorker;
?
const staff: IStaff = {
id: 'E1006',
age: 33,
companyId: 'EFT'
};
?
console.dir(staff)
在上面示例中,我們首先為 IPerson 和 IWorker 型別定義了不同的成員,然后通過 & 運算子定義了 IStaff 交叉型別,所以該型別同時擁有 IPerson 和 IWorker 這兩種型別的成員,
七、TypeScript 函式
7.1 TypeScript 函式與 JavaScript 函式的區別

7.2 箭頭函式
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.使用示例
?
// 未使用箭頭函式
function Book() {
let self = this;
self.publishDate = 2016;
setInterval(function () {
console.log(self.publishDate);
}, 1000);
}
?
// 使用箭頭函式
function Book() {
this.publishDate = 2016;
setInterval(() => {
console.log(this.publishDate);
}, 1000);
}
7.3 引數型別和回傳型別
?
function createUserId(name: string, id: number): string {
return name + id;
}
7.4 函式型別
?
let IdGenerator: (chars: string, nums: number) => string;
?
function createUserId(name: string, id: number): string {
return name + id;
}
?
IdGenerator = createUserId;
7.5 可選引數及默認引數
?
// 可選引數
function createUserId(name: string, id: number, age?: number): string {
return name + id;
}
?
// 默認引數
function createUserId(
name: string = "Semlinker",
id: number,
age?: number
): string {
return name + id;
}
在宣告函式時,可以通過 ? 號來定義可選引數,比如 age?: number 這種形式,在實際使用時,需要注意的是可選引數要放在普通引數的后面,不然會導致編譯錯誤,
7.6 剩余引數
?
function push(array, ...items) {
items.forEach(function (item) {
array.push(item);
});
}
?
let a = [];
push(a, 1, 2, 3);
7.7 函式多載
函式多載或方法多載是使用相同名稱和不同引數數量或型別創建多個方法的一種能力,要解決前面遇到的問題,方法就是為同一個函式提供多個函式型別定義來進行函式多載,編譯器會根據這個串列去處理函式的呼叫,
?
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) {
if (typeof a === "string" || typeof b === "string") {
return a.toString() + b.toString();
}
return a + b;
}
在以上代碼中,我們為 add 函式提供了多個函式型別定義,從而實作函式的多載,之后,可惡的錯誤訊息又消失了,因為這時 result 變數的型別是 string 型別,在 TypeScript 中除了可以多載普通函式之外,我們還可以多載類中的成員方法,
方法多載是指在同一個類中方法同名,引數不同(引數型別不同、引數個數不同或引數個數相同時引數的先后順序不同),呼叫時根據實參的形式,選擇與它匹配的方法執行操作的一種技術,所以類中成員方法滿足多載的條件是:在同一個類中,方法名相同且引數串列不同,下面我們來舉一個成員方法多載的例子:
?
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 編譯器處理函式多載時,它會查找多載串列,嘗試使用第一個多載定義, 如果匹配的話就使用這個, 因此,在定義多載的時候,一定要把最精確的定義放在最前面,另外在 Calculator 類中,add(a: Combinable, b: Combinable){ } 并不是多載串列的一部分,因此對于 add 成員方法來說,我們只定義了四個多載方法,
八、TypeScript 陣列
8.1 陣列解構
?
let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;
8.2 陣列展開運算子
let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];
8.3 陣列遍歷
let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
console.log(i);
}
九、TypeScript 物件
9.1 物件解構
?
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 介面
在面向物件語言中,介面是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類去實作,
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;
}
只讀屬性用于限制只能在物件剛剛創建的時候修改其值,此外 TypeScript 還提供了 ReadonlyArray 型別,它與 Array 相似,只是把所有可變方法去掉了,因此可以確保陣列創建后再也不能被修改,
?
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!
十一、TypeScript 類
11.1 類的屬性與方法
在面向物件語言中,類是一種面向物件計算機編程語言的構造,是創建物件的藍圖,描述了所創建的物件共同的屬性和方法,
在 TypeScript 中,我們可以通過 Class 關鍵字來定義一個類:
?
class Greeter {
// 靜態屬性
static cname: string = "Greeter";
// 成員屬性
greeting: string;
?
// 建構式 - 執行初始化操作
constructor(message: string) {
this.greeting = message;
}
?
// 靜態方法
static getClassName() {
return "Class name is Greeter";
}
?
// 成員方法
greet() {
return "Hello, " + this.greeting;
}
}
?
let greeter = new Greeter("world");
那么成員屬性與靜態屬性,成員方法與靜態方法有什么區別呢?這里無需過多解釋,我們直接看一下以下編譯生成的 ES5 代碼:
?
"use strict";
var Greeter = /** @class */ (function () {
// 建構式 - 執行初始化操作
function Greeter(message) {
this.greeting = message;
}
// 靜態方法
Greeter.getClassName = function () {
return "Class name is Greeter";
};
// 成員方法
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
// 靜態屬性
Greeter.cname = "Greeter";
return Greeter;
}());
var greeter = new Greeter("world");
11.2 訪問器
在 TypeScript 中,我們可以通過 getter 和 setter 方法來實作資料的封裝和有效性校驗,防止出現例外資料,
?
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.3 類的繼承
繼承 (Inheritance) 是一種聯結類與類的層次模型,指的是一個類(稱為子類、子介面)繼承另外的一個類(稱為父類、父介面)的功能,并可以增加它自己的新功能的能力,繼承是類與類或者介面與介面之間最常見的關系,
繼承是一種 is-a 關系:

在 TypeScript 中,我們可以通過 extends 關鍵字來實作繼承:
?
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);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
?
let sam = new Snake("Sammy the Python");
sam.move();
11.4 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.
與常規屬性(甚至使用 private 修飾符宣告的屬性)不同,私有欄位要牢記以下規則:
-
私有欄位以
#字符開頭,有時我們稱之為私有名稱; -
每個私有欄位名稱都唯一地限定于其包含的類;
-
不能在私有欄位上使用 TypeScript 可訪問性修飾符(如 public 或 private);
-
私有欄位不能在包含的類之外訪問,甚至不能被檢測到,
十二、TypeScript 泛型
軟體工程中,我們不僅要創建一致的定義良好的 API,同時也要考慮可重用性, 組件不僅能夠支持當前的資料型別,同時也能支持未來的資料型別,這在創建大型系統時為你提供了十分靈活的功能,
在像 C# 和 Java 這樣的語言中,可以使用泛型來創建可重用的組件,一個組件可以支持多種型別的資料, 這樣用戶就可以以自己的資料型別來使用組件,
設計泛型的關鍵目的是在成員之間提供有意義的約束,這些成員可以是:類的實體成員、類的方法、函式引數和函式回傳值,
泛型(Generics)是允許同一個函式接受不同型別引數的一種模板,相比于使用 any 型別,使用泛型來創建可復用的組件要更好,因為泛型會保留引數型別,
12.1 泛型介面
interface GenericIdentityFn<T> { (arg: T): T;}
12.2 泛型類
?
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.3 泛型變數
對剛接觸 TypeScript 泛型的小伙伴來說,看到 T 和 E,還有 K 和 V 這些泛型變數時,估計會一臉懵逼,其實這些大寫字母并沒有什么本質的區別,只不過是一個約定好的規范而已,也就是說使用大寫字母 A-Z 定義的型別變數都屬于泛型,把 T 換成 A,也是一樣的,下面我們介紹一下一些常見泛型變數代表的意思:
-
T(Type):表示一個 TypeScript 型別
-
K(Key):表示物件中的鍵型別
-
V(Value):表示物件中的值型別
-
E(Element):表示元素型別
12.4 泛型工具型別
為了方便開發者 TypeScript 內置了一些常用的工具型別,比如 Partial、Required、Readonly、Record 和 ReturnType 等,出于篇幅考慮,這里我們只簡單介紹 Partial 工具型別,不過在具體介紹之前,我們得先介紹一些相關的基礎知識,方便讀者自行學習其它的工具型別,
1.typeof
在 TypeScript 中,typeof 運算子可以用來獲取一個變數宣告或物件的型別,
?
interface Person {
name: string;
age: number;
}
?
const sem: Person = { name: 'semlinker', age: 30 };
type Sem= typeof sem; // -> Person
?
function toArray(x: number): Array<number> {
return [x];
}
?
type Func = typeof toArray; // -> (x: number) => number[]
2.keyof
keyof 運算子可以用來一個物件中的所有 key 值:
?
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
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 就是宣告一個變數來承載傳入函式簽名的回傳值型別,簡單說就是用它取到函式回傳值的型別方便之后使用,
5.extends
有時候我們定義的泛型不想過于靈活或者說想繼承某些類等,可以通過 extends 關鍵字添加泛型約束,
?
interface ILengthwise {
length: number;
}
?
function loggingIdentity<T extends ILengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
現在這個泛型函式被定義了約束,因此它不再是適用于任意型別:
loggingIdentity(3); // Error, number doesn't have a .length property
這時我們需要傳入符合約束型別的值,必須包含必須的屬性:
loggingIdentity({length: 10, value: 3});
6.Partial
Partial 的作用就是將某個型別里的屬性全部變為可選項 ?,
定義:
?
/**
* 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] 取得相應的屬性值,中間的 ? 號,用于將所有屬性變為可選,
示例:
?
interface Todo {
title: string;
description: string;
}
?
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
?
const todo1 = {
title: "organize desk",
description: "clear clutter",
};
?
const todo2 = updateTodo(todo1, {
description: "throw out trash",
});
在上面的 updateTodo 方法中,我們利用 Partial 工具型別,定義 fieldsToUpdate 的型別為 Partial,即:
{ title?: string | undefined; description?: string | undefined;}
十三、TypeScript 裝飾器
13.1 裝飾器是什么
-
它是一個運算式
-
該運算式被執行后,回傳一個函式
-
函式的入參分別為 target、name 和 descriptor
-
執行該函式后,可能回傳 descriptor 物件,用于配置 target 物件
13.2 裝飾器的分類
-
類裝飾器(Class decorators)
-
屬性裝飾器(Property decorators)
-
方法裝飾器(Method decorators)
-
引數裝飾器(Parameter decorators)
13.3 類裝飾器
類裝飾器宣告:
?
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
類裝飾器顧名思義,就是用來裝飾類的,它接收一個引數:
-
target: TFunction - 被裝飾的類
看完第一眼后,是不是感覺都不好了,沒事,我們馬上來個例子:
?
function Greeter(target: Function): void {
target.prototype.greet = function (): void {
console.log("Hello Semlinker!");
};
}
?
@Greeter
class Greeting {
constructor() {
// 內部實作
}
}
?
let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello Semlinker!';
上面的例子中,我們定義了 Greeter 類裝飾器,同時我們使用了 @Greeter 語法糖,來使用裝飾器,
友情提示:讀者可以直接復制上面的代碼,在 TypeScript Playground 中運行查看結果,
有的讀者可能想問,例子中總是輸出 Hello Semlinker! ,能自定義輸出的問候語么 ?這個問題很好,答案是可以的,
具體實作如下:
?
function Greeter(greeting: string) {
return function (target: Function) {
target.prototype.greet = function (): void {
console.log(greeting);
};
};
}
?
@Greeter("Hello TS!")
class Greeting {
constructor() {
// 內部實作
}
}
?
let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello TS!';
13.4 屬性裝飾器
屬性裝飾器宣告:
?
declare type PropertyDecorator = (target:Object,
propertyKey: string | symbol ) => void;
屬性裝飾器顧名思義,用來裝飾類的屬性,它接收兩個引數:
-
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 函式,來跟蹤用戶對屬性的操作,當代碼成功運行后,在控制臺會輸出以下結果:
Set: name => semlinkerSet: name => kakuqo
13.5 方法裝飾器
方法裝飾器宣告:
?
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
方法裝飾器顧名思義,用來裝飾類的方法,它接收三個引數:
-
target: Object - 被裝飾的類
-
propertyKey: string | symbol - 方法名
-
descriptor: TypePropertyDescript - 屬性描述符
廢話不多說,直接上例子:
?
function LogOutput(tarage: Function, key: string, descriptor: any) {
let originalMethod = descriptor.value;
let newMethod = function(...args: any[]): any {
let result: any = originalMethod.apply(this, args);
if(!this.loggedOutput) {
this.loggedOutput = new Array<any>();
}
this.loggedOutput.push({
method: key,
parameters: args,
output: result,
timestamp: new Date()
});
return result;
};
descriptor.value = newMethod;
}
?
class Calculator {
@LogOutput
double (num: number): number {
return num * 2;
}
}
?
let calc = new Calculator();
calc.double(11);
// console ouput: [{method: "double", output: 22, ...}]
console.log(calc.loggedOutput);
下面我們來介紹一下引數裝飾器,
13.6 引數裝飾器
引數裝飾器宣告:
?
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
parameterIndex: number ) => void
引數裝飾器顧名思義,是用來裝飾函式引數,它接收三個引數:
-
target: Object - 被裝飾的類
-
propertyKey: string | symbol - 方法名
-
parameterIndex: number - 方法中引數的索引值
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; }}// console output: The parameter in position 0 // at Greeter has been decorated
介紹完 TypeScript 入門相關的基礎知識,猜測很多剛入門的小伙伴已有 “從入門到放棄” 的想法,最后我們來簡單介紹一下編譯背景關系,
十四、編譯背景關系
14.1 tsconfig.json 的作用
-
用于標識 TypeScript 專案的根路徑;
-
用于配置 TypeScript 編譯器;
-
用于指定編譯的檔案,
14.2 tsconfig.json 重要欄位
-
files - 設定要編譯的檔案的名稱;
-
include - 設定需要進行編譯的檔案,支持路徑模式匹配;
-
exclude - 設定無需進行編譯的檔案,支持路徑模式匹配;
-
compilerOptions - 設定與編譯流程相關的選項,
14.3 compilerOptions 選項
compilerOptions 支持很多選項,常見的有 baseUrl、 target、baseUrl、 moduleResolution 和 lib 等,
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, // 生成相應的 '.d.ts' 檔案
"sourceMap": true, // 生成相應的 '.map' 檔案
"outFile": "./", // 將輸出檔案合并為一個檔案
"outDir": "./", // 指定輸出目錄
"rootDir": "./", // 用來控制輸出目錄結構 --outDir.
"removeComments": true, // 洗掉編譯后的所有的注釋
"noEmit": true, // 不生成輸出檔案
"importHelpers": true, // 從 tslib 匯入輔助工具函式
"isolatedModules": true, // 將每個檔案做為單獨的模塊 (與 'ts.transpileModule' 類似).
?
/* 嚴格的型別檢查選項 */
"strict": true, // 啟用所有嚴格型別檢查選項
"noImplicitAny": true, // 在運算式和宣告上有隱含的 any型別時報錯
"strictNullChecks": true, // 啟用嚴格的 null 檢查
"noImplicitThis": true, // 當 this 運算式值為 any 型別的時候,生成一個錯誤
"alwaysStrict": true, // 以嚴格模式檢查每個模塊,并在每個檔案里加入 'use strict'
?
/* 額外的檢查 */
"noUnusedLocals": true, // 有未使用的變數時,拋出錯誤
"noUnusedParameters": true, // 有未使用的引數時,拋出錯誤
"noImplicitReturns": true, // 并不是所有函式里的代碼都有回傳值時,拋出錯誤
"noFallthroughCasesInSwitch": true, // 報告 switch 陳述句的 fallthrough 錯誤,(即,不允許 switch 的 case 陳述句貫穿)
?
/* 模塊決議選項 */
"moduleResolution": "node", // 選擇模塊決議策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于決議非相對模塊名稱的基目錄
"paths": {}, // 模塊名到基于 baseUrl 的路徑映射的串列
"rootDirs": [], // 根檔案夾串列,其組合內容表示專案運行時的結構內容
"typeRoots": [], // 包含型別宣告的檔案串列
"types": [], // 需要包含的型別宣告檔案名串列
"allowSyntheticDefaultImports": true, // 允許從沒有設定默認匯出的模塊中默認匯入,
?
/* Source Map Options */
"sourceRoot": "./", // 指定除錯器應該找到 TypeScript 檔案而不是源檔案的位置
"mapRoot": "./", // 指定除錯器應該找到映射檔案而不是生成檔案的位置
"inlineSourceMap": true, // 生成單個 soucemaps 檔案,而不是將 sourcemaps 生成不同的檔案
"inlineSources": true, // 將代碼與 sourcemaps 生成到一個檔案中,要求同時設定了 --inlineSourceMap 或 --sourceMap 屬性
?
/* 其他選項 */
"experimentalDecorators": true, // 啟用裝飾器
"emitDecoratorMetadata": true // 為裝飾器提供元資料的支持
}
}
總結
對了,小編為大家準備了一套2020最新的web前端資料,需要點擊下方鏈接獲取方式
學習前端,你掌握這些,二線也能輕松拿8K以上
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/49997.html
標籤:JavaScript
