您好,登錄后才能下訂單哦!
如何正確的使用TypeScript,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
一、TypeScript 是什么
TypeScript 是一種由微軟開發的自由和開源的編程語言。它是 JavaScript 的一個超集,而且本質上向這個語言添加了可選的靜態類型和基于類的面向對象編程。
TypeScript 提供最新的和不斷發展的 JavaScript 特性,包括那些來自 2015 年的 ECMAScript 和未來的提案中的特性,比如異步功能和 Decorators,以幫助建立健壯的組件。下圖顯示了 TypeScript 與 ES5、ES2015 和 ES2016 之間的關系:
1.1 TypeScript 與 JavaScript 的區別
TypeScript | JavaScript |
---|---|
JavaScript 的超集用于解決大型項目的代碼復雜性 | 一種腳本語言,用于創建動態網頁。 |
可以在編譯期間發現并糾正錯誤 | 作為一種解釋型語言,只能在運行時發現錯誤 |
強類型,支持靜態和動態類型 | 弱類型,沒有靜態類型選項 |
最終被編譯成 JavaScript 代碼,使瀏覽器可以理解 | 可以直接在瀏覽器中使用 |
支持模塊、泛型和接口 | 不支持模塊,泛型或接口 |
支持 ES3,ES4,ES5 和 ES6 等 | 不支持編譯其他 ES3,ES4,ES5 或 ES6 功能 |
社區的支持仍在增長,而且還不是很大 | 大量的社區支持以及大量文檔和解決問題的支持 |
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 Playground:https://www.typescriptlang.org/play/
二、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: DirectionDirection = Direction.NORTH;
默認情況下,NORTH 的初始值為 0,其余的成員會從 1 開始自動增長。換句話說,Direction.SOUTH 的值為 1,Direction.EAST 的值為 2,Direction.WEST 的值為 3。上面的枚舉示例代碼經過編譯后會生成以下代碼:
"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;
當然我們也可以設置 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 代碼如下:
"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 代碼,我們可以發現數字枚舉相對字符串枚舉多了 “反向映射”:
console.log(Enum.A) //輸出:0 console.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; // OK value.trim(); // OK value(); // OK new value(); // OK value[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; // 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 中存儲了什么類型的值。
現在讓我們看看當我們嘗試對類型為 unknown 的值執行操作時會發生什么。以下是我們在之前 any 章節看過的相同操作:
let value: unknown; value.foo.bar; // Error value.trim(); // Error value(); // Error new value(); // Error value[0][1]; // 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]); // Semlinker console.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:
// 聲明函數返回值為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 或 null:
let unusable: void = undefined;
2.10 Null 和 Undefined 類型
TypeScript 里,undefined 和 null 兩者有各自的類型分別為 undefined 和 null。
let u: undefinedundefined = undefined; let n: nullnull = 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 函數的區別
TypeScript | JavaScript |
---|---|
含有類型 | 無類型 |
箭頭函數 | 箭頭函數(ES2015) |
函數類型 | 無函數類型 |
必填和可選參數 | 所有參數都是可選的 |
默認參數 | 默認參數 |
剩余參數 | 剩余參數 |
函數重載 | 無函數重載 |
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<T> 類型,它與 Array<T> 相似,只是把所有可變方法去掉了,因此可以確保數組創建后再也不能被修改。
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.#namename = 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<T> 的作用就是將某個類型里的屬性全部變為可選項 ?。
定義:
/** * 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<T> 工具類型,定義 fieldsToUpdate 的類型為 Partial<Todo>,即:
{ 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 => semlinker Set: 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 // 為裝飾器提供元數據的支持 } }
關于如何正確的使用TypeScript問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。