阿寶哥第一次使用 TypeScript 是在 Angular 2.x 項目中,那時候 TypeScript 尚未進入大衆的視野。然而如今學習 TypeScript 的小夥伴愈來愈多了,本文阿寶哥將從 16 個方面入手,帶你一步步學習 TypeScript,感興趣的小夥伴不要錯過。javascript
TypeScript 是一種由微軟開發的自由和開源的編程語言。它是 JavaScript 的一個超集,並且本質上向這個語言添加了可選的靜態類型和基於類的面向對象編程。html
TypeScript 提供最新的和不斷髮展的 JavaScript 特性,包括那些來自 2015 年的 ECMAScript 和將來的提案中的特性,好比異步功能和 Decorators,以幫助創建健壯的組件。下圖顯示了 TypeScript 與 ES五、ES2015 和 ES2016 之間的關係:java
TypeScript | JavaScript |
---|---|
JavaScript 的超集用於解決大型項目的代碼複雜性 | 一種腳本語言,用於建立動態網頁 |
能夠在編譯期間發現並糾正錯誤 | 做爲一種解釋型語言,只能在運行時發現錯誤 |
強類型,支持靜態和動態類型 | 弱類型,沒有靜態類型選項 |
最終被編譯成 JavaScript 代碼,使瀏覽器能夠理解 | 能夠直接在瀏覽器中使用 |
支持模塊、泛型和接口 | 不支持模塊,泛型或接口 |
社區的支持仍在增加,並且還不是很大 | 大量的社區支持以及大量文檔和解決問題的支持 |
命令行的 TypeScript 編譯器可使用 npm 包管理器來安裝。node
$ npm install -g typescript
複製代碼
$ tsc -v
# Version 4.0.2
複製代碼
$ tsc helloworld.ts
# helloworld.ts => helloworld.js
複製代碼
固然,對剛入門 TypeScript 的小夥伴來講,也能夠不用安裝 typescript
,而是直接使用線上的 TypeScript Playground 來學習新的語法或新特性。經過配置 TS Config 的 Target,能夠設置不一樣的編譯目標,從而編譯生成不一樣的目標代碼。react
下圖示例中所設置的編譯目標是 ES5:git
(圖片來源:www.typescriptlang.org/play)github
如你所見,在上圖中包含 3 個 ts 文件:a.ts、b.ts 和 c.ts。這些文件將被 TypeScript 編譯器,根據配置的編譯選項編譯成 3 個 js 文件,即 a.js、b.js 和 c.js。對於大多數使用 TypeScript 開發的 Web 項目,咱們還會對編譯生成的 js 文件進行打包處理,而後在進行部署。typescript
新建一個 hello.ts
文件,並輸入如下內容:shell
function greet(person: string) {
return 'Hello, ' + person;
}
console.log(greet("TypeScript"));
複製代碼
而後執行 tsc hello.ts
命令,以後會生成一個編譯好的文件 hello.js
:數據庫
"use strict";
function greet(person) {
return 'Hello, ' + person;
}
console.log(greet("TypeScript"));
複製代碼
觀察以上編譯後的輸出結果,咱們發現 person
參數的類型信息在編譯後被擦除了。TypeScript 只會在編譯階段對類型進行靜態檢查,若是發現有錯誤,編譯時就會報錯。而在運行時,編譯生成的 JS 與普通的 JavaScript 文件同樣,並不會進行類型檢查。
let isDone: boolean = false;
// ES5:var isDone = false;
複製代碼
let count: number = 10;
// ES5:var count = 10;
複製代碼
let name: string = "semliker";
// ES5:var name = 'semlinker';
複製代碼
const sym = Symbol();
let obj = {
[sym]: "semlinker",
};
console.log(obj[sym]); // semlinker
複製代碼
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];
複製代碼
使用枚舉咱們能夠定義一些帶名字的常量。 使用枚舉能夠清晰地表達意圖或建立一組有區別的用例。 TypeScript 支持數字的和基於字符串的枚舉。
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
複製代碼
默認狀況下,NORTH 的初始值爲 0,其他的成員會從 1 開始自動增加。換句話說,Direction.SOUTH 的值爲 1,Direction.EAST 的值爲 2,Direction.WEST 的值爲 3。
以上的枚舉示例經編譯後,對應的 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;
複製代碼
固然咱們也能夠設置 NORTH 的初始值,好比:
enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST,
}
複製代碼
在 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 = {}));
複製代碼
經過觀察數字枚舉和字符串枚舉的編譯結果,咱們能夠知道數字枚舉除了支持 從成員名稱到成員值 的普通映射以外,它還支持 從成員值到成員名稱 的反向映射:
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dirName = Direction[0]; // NORTH
let dirVal = Direction["NORTH"]; // 0
複製代碼
另外,對於純字符串枚舉,咱們不能省略任何初始化程序。而數字枚舉若是沒有顯式設置值時,則會使用默認規則進行初始化。
除了數字枚舉和字符串枚舉以外,還有一種特殊的枚舉 —— 常量枚舉。它是使用 const
關鍵字修飾的枚舉,常量枚舉會使用內聯語法,不會爲枚舉類型編譯生成任何 JavaScript。爲了更好地理解這句話,咱們來看一個具體的例子:
const enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
複製代碼
以上代碼對應的 ES5 代碼以下:
"use strict";
var dir = 0 /* NORTH */;
複製代碼
異構枚舉的成員值是數字和字符串的混合:
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
複製代碼
在 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
類型。
就像全部類型均可以賦值給 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
類型,咱們已將容許全部更改的默認設置,更改成禁止任何更改。
衆所周知,數組通常由同種類型的值組成,但有時咱們須要在單個變量中存儲不一樣類型的值,這時候咱們就可使用元組。在 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]'.
複製代碼
某種程度上來講,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
:
let unusable: void = undefined;
複製代碼
TypeScript 裏,undefined
和 null
二者有各自的類型分別爲 undefined
和 null
。
let u: undefined = undefined;
let n: null = null;
複製代碼
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
複製代碼
Object 類型:它是全部 Object 類的實例的類型,它由如下兩個接口來定義:
// 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;
}
複製代碼
// 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 接口中的全部屬性。
{} 類型描述了一個沒有成員的對象。當你試圖訪問這樣一個對象的任意屬性時,TypeScript 會產生一個編譯時錯誤。
// Type {}
const obj = {};
// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";
複製代碼
可是,你仍然可使用在 Object 類型上定義的全部屬性和方法,這些屬性和方法可經過 JavaScript 的原型鏈隱式地使用:
// Type {}
const obj = {};
// "[object Object]"
obj.toString();
複製代碼
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 更瞭解某個值的詳細信息。一般這會發生在你清楚地知道一個實體具備比它現有類型更確切的類型。
經過類型斷言這種方式能夠告訴編譯器,「相信我,我知道本身在幹什麼」。類型斷言比如其餘語言裏的類型轉換,可是不進行特殊的數據檢查和解構。它沒有運行時的影響,只是在編譯階段起做用。
類型斷言有兩種形式:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
複製代碼
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
複製代碼
在上下文中當類型檢查器沒法判定類型時,一個新的後綴表達式操做符 !
能夠用於斷言操做對象是非 null 和非 undefined 類型。具體而言,x! 將從 x 值域中排除 null 和 undefined 。
那麼非空斷言操做符到底有什麼用呢?下面咱們先來看一下非空斷言操做符的一些使用場景。
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
}
複製代碼
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 代碼中,!
非空斷言操做符被移除了,因此在瀏覽器中執行以上代碼,在控制檯會輸出 undefined
。
在 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 編譯器就會知道該屬性會被明確地賦值。
類型保護是可執行運行時檢查的一種表達式,用於確保該類型在必定的範圍內。 換句話說,類型保護能夠保證一個字符串是一個字符串,儘管它的值也能夠是一個數值。類型保護與特性檢測並非徹底不一樣,其主要思想是嘗試檢測屬性、方法或原型,以肯定如何處理值。目前主要有四種的方式來實現類型保護:
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);
}
}
複製代碼
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 並不會阻止你與其它字符串比較,語言不會把那些表達式識別爲類型保護。
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'
}
複製代碼
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
複製代碼
聯合類型一般與 null
或 undefined
一塊兒使用:
const sayHello = (name: string | undefined) => {
/* ... */
};
複製代碼
例如,這裏 name
的類型是 string | undefined
意味着能夠將 string
或 undefined
的值傳遞給sayHello
函數。
sayHello("semlinker");
sayHello(undefined);
複製代碼
經過這個示例,你能夠憑直覺知道類型 A 和類型 B 聯合後的類型是同時接受 A 和 B 值的類型。此外,對於聯合類型來講,你可能會遇到如下的用法:
let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';
複製代碼
以上示例中的 1
、2
或 'click'
被稱爲字面量類型,用來約束取值只能是某幾個值中的一個。
TypeScript 可辨識聯合(Discriminated Unions)類型,也稱爲代數數據類型或標籤聯合類型。它包含 3 個要點:可辨識、聯合類型和類型守衛。
這種類型的本質是結合聯合類型和字面量類型的一種類型保護方法。若是一個類型是多個類型的聯合類型,且多個類型含有一個公共屬性,那麼就能夠利用這個公共屬性,來建立不一樣的類型保護區塊。
可辨識要求聯合類型中的每一個元素都含有一個單例類型屬性,好比:
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
屬性,該屬性被稱爲可辨識的屬性,而其它的屬性只跟特性的接口相關。
基於前面定義了三個接口,咱們能夠建立一個 Vehicle
聯合類型:
type Vehicle = Motorcycle | Car | Truck;
複製代碼
如今咱們就能夠開始使用 Vehicle
聯合類型,對於 Vehicle
類型的變量,它能夠表示不一樣類型的車輛。
下面咱們來定義一個 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
對象中的所包含的屬性,來正確的計算該車輛類型所對應的價格。
類型別名用來給一個類型起個新名字。
type Message = string | string[];
let greet = (message: Message) => {
// ...
};
複製代碼
在 TypeScript 中交叉類型是將多個類型合併爲一個類型。經過 &
運算符能夠將現有的多種類型疊加到一塊兒成爲一種類型,它包含了所需的全部類型的特性。
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
let point: Point = {
x: 1,
y: 1
}
複製代碼
在上面代碼中咱們先定義了 PartialPointX
類型,接着使用 &
運算符建立一個新的 Point
類型,表示一個含有 x 和 y 座標的點,而後定義了一個 Point
類型的變量並初始化。
那麼如今問題來了,假設在合併多個類型的過程當中,恰好出現某些類型存在相同的成員,但對應的類型又不一致,好比:
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 的類型是否是能夠是 string
或 number
類型呢?好比下面的例子:
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
。
在上面示例中,恰好接口 X 和接口 Y 中內部成員 c 的類型都是基本數據類型,那麼若是是非基本數據類型的話,又會是什麼情形。咱們來看個具體的例子:
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);
複製代碼
以上代碼成功運行後,控制檯會輸出如下結果:
由上圖可知,在混入多個類型時,若存在相同的成員,且成員類型爲非基本數據類型,那麼是能夠成功合併。
TypeScript | JavaScript |
---|---|
含有類型 | 無類型 |
箭頭函數 | 箭頭函數(ES2015) |
函數類型 | 無函數類型 |
必填和可選參數 | 全部參數都是可選的 |
默認參數 | 默認參數 |
剩餘參數 | 剩餘參數 |
函數重載 | 無函數重載 |
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);
});
複製代碼
// 未使用箭頭函數
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);
}
複製代碼
function createUserId(name: string, id: number): string {
return name + id;
}
複製代碼
let IdGenerator: (chars: string, nums: number) => string;
function createUserId(name: string, id: number): string {
return name + id;
}
IdGenerator = createUserId;
複製代碼
// 可選參數
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
這種形式。在實際使用時,須要注意的是可選參數要放在普通參數的後面,否則會致使編譯錯誤。
function push(array, ...items) {
items.forEach(function (item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
複製代碼
函數重載或方法重載是使用相同名稱和不一樣參數數量或類型建立多個方法的一種能力。
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;
}
複製代碼
在以上代碼中,咱們爲 add 函數提供了多個函數類型定義,從而實現函數的重載。在 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 成員方法來講,咱們只定義了四個重載方法。
let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;
複製代碼
let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];
複製代碼
let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
console.log(i);
}
複製代碼
let person = {
name: "Semlinker",
gender: "Male",
};
let { name, gender } = person;
複製代碼
let person = {
name: "Semlinker",
gender: "Male",
address: "Xiamen",
};
// 組裝對象
let personWithAge = { ...person, age: 33 };
// 獲取除了某些項外的其它項
let { name, ...rest } = person;
複製代碼
在面嚮對象語言中,接口是一個很重要的概念,它是對行爲的抽象,而具體如何行動須要由類去實現。
TypeScript 中的接口是一個很是靈活的概念,除了可用於對類的一部分行爲進行抽象之外,也經常使用於對「對象的形狀(Shape)」進行描述。
interface Person {
name: string;
age: number;
}
let semlinker: Person = {
name: "semlinker",
age: 33,
};
複製代碼
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!
複製代碼
有時候咱們但願一個接口中除了包含必選和可選屬性以外,還容許有其餘的任意屬性,這時咱們可使用 索引簽名 的形式來知足上述要求。
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 }
複製代碼
接口和類型別名均可以用來描述對象的形狀或函數簽名:
接口
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;
複製代碼
與接口類型不同,類型別名能夠用於一些其餘類型,好比原始類型、聯合類型和元組:
// primitive
type Name = string;
// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
複製代碼
接口和類型別名都可以被擴展,但語法有所不一樣。此外,接口和類型別名不是互斥的。接口能夠擴展類型別名,而反過來是不行的。
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; };
複製代碼
類能夠以相同的方式實現接口或類型別名,但類不能實現使用類型別名定義的聯合類型:
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;
}
複製代碼
與類型別名不一樣,接口能夠定義屢次,會被自動合併爲單個接口。
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };
複製代碼
在面嚮對象語言中,類是一種面向對象計算機編程語言的構造,是建立對象的藍圖,描述了所建立的對象共同的屬性和方法。
在 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");
複製代碼
在 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 中,咱們能夠經過 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);
}
複製代碼
繼承(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();
複製代碼
使用 abstract
關鍵字聲明的類,咱們稱之爲抽象類。抽象類不能被實例化,由於它裏面包含一個或多個抽象方法。所謂的抽象方法,是指不包含具體實現的方法:
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
複製代碼
抽象類不能被直接實例化,咱們只能實例化實現了全部抽象方法的子類。具體以下所示:
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!
複製代碼
在前面的章節,咱們已經介紹了函數重載。對於類的方法來講,它也支持重載。好比,在如下示例中咱們重載了 ProductService
類的 getProducts
成員方法:
class ProductService {
getProducts(): void;
getProducts(id: number): void;
getProducts(id?: number) {
if(typeof id === 'number') {
console.log(`獲取id爲 ${id} 的產品信息`);
} else {
console.log(`獲取全部的產品信息`);
}
}
}
const productService = new ProductService();
productService.getProducts(666); // 獲取id爲 666 的產品信息
productService.getProducts(); // 獲取全部的產品信息
複製代碼
軟件工程中,咱們不只要建立一致的定義良好的 API,同時也要考慮可重用性。 組件不只可以支持當前的數據類型,同時也能支持將來的數據類型,這在建立大型系統時爲你提供了十分靈活的功能。
在像 C# 和 Java 這樣的語言中,可使用泛型來建立可重用的組件,一個組件能夠支持多種類型的數據。 這樣用戶就能夠以本身的數據類型來使用組件。
設計泛型的關鍵目的是在成員之間提供有意義的約束,這些成員能夠是:類的實例成員、類的方法、函數參數和函數返回值。
泛型(Generics)是容許同一個函數接受不一樣類型參數的一種模板。相比於使用 any 類型,使用泛型來建立可複用的組件要更好,由於泛型會保留參數類型。
對於剛接觸 TypeScript 泛型的讀者來講,首次看到 <T>
語法會感到陌生。其實它沒有什麼特別,就像傳遞參數同樣,咱們傳遞了咱們想要用於特定函數調用的類型。
參考上面的圖片,當咱們調用 identity<Number>(1)
,Number
類型就像參數 1
同樣,它將在出現 T
的任何位置填充該類型。圖中 <T>
內部的 T
被稱爲類型變量,它是咱們但願傳遞給 identity 函數的類型佔位符,同時它被分配給 value
參數用來代替它的類型:此時 T
充當的是類型,而不是特定的 Number 類型。
其中 T
表明 Type,在定義泛型時一般用做第一個類型變量名稱。但實際上 T
能夠用任何有效名稱代替。除了 T
以外,如下是常見泛型變量表明的意思:
其實並非只能定義一個類型變量,咱們能夠引入但願定義的任何數量的類型變量。好比咱們引入一個新的類型變量 U
,用於擴展咱們定義的 identity
函數:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
複製代碼
除了爲類型變量顯式設定值以外,一種更常見的作法是使編譯器自動選擇這些類型,從而使代碼更簡潔。咱們能夠徹底省略尖括號,好比:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity(68, "Semlinker"));
複製代碼
對於上述代碼,編譯器足夠聰明,可以知道咱們的參數類型,並將它們賦值給 T 和 U,而不須要開發人員顯式指定它們。
interface GenericIdentityFn<T> {
(arg: T): T;
}
複製代碼
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;
};
複製代碼
爲了方便開發者 TypeScript 內置了一些經常使用的工具類型,好比 Partial、Required、Readonly、Record 和 ReturnType 等。出於篇幅考慮,這裏咱們只簡單介紹 Partial 工具類型。不過在具體介紹以前,咱們得先介紹一些相關的基礎知識,方便讀者自行學習其它的工具類型。
在 TypeScript 中,typeof
操做符能夠用來獲取一個變量聲明或對象的類型。
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[]
複製代碼
keyof
操做符是在 TypeScript 2.1 版本引入的,該操做符能夠用於獲取某種類型的全部鍵,其返回類型是聯合類型。
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 中支持兩種索引簽名,數字索引和字符串索引:
interface StringArray {
// 字符串索引 -> keyof StringArray => string | number
[index: string]: string;
}
interface StringArray1 {
// 數字索引 -> keyof StringArray1 => number
[index: number]: string;
}
複製代碼
爲了同時支持兩種索引類型,就得要求數字索引的返回值必須是字符串索引返回值的子類。其中的緣由就是當使用數值索引時,JavaScript 在執行索引操做時,會先把數值索引先轉換爲字符串索引。因此 keyof { [x: string]: Person }
的結果會返回 string | number
。
in
用來遍歷枚舉類型:
type Keys = "a" | "b" | "c"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }
複製代碼
在條件類型語句中,能夠用 infer
聲明一個類型變量而且對它進行使用。
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;
複製代碼
以上代碼中 infer R
就是聲明一個變量來承載傳入函數簽名的返回值類型,簡單說就是用它取到函數返回值的類型方便以後使用。
有時候咱們定義的泛型不想過於靈活或者說想繼承某些類等,能夠經過 extends 關鍵字添加泛型約束。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
複製代碼
如今這個泛型函數被定義了約束,所以它再也不是適用於任意類型:
loggingIdentity(3); // Error, number doesn't have a .length property
複製代碼
這時咱們須要傳入符合約束類型的值,必須包含必須的屬性:
loggingIdentity({length: 10, value: 3});
複製代碼
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: "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;
}
複製代碼
須要注意的是,若要啓用實驗性的裝飾器特性,你必須在命令行或 tsconfig.json
裏啓用 experimentalDecorators
編譯器選項:
命令行:
tsc --target ES5 --experimentalDecorators
複製代碼
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
複製代碼
類裝飾器聲明:
declare type ClassDecorator = <TFunction extends Function>( target: TFunction ) => TFunction | void; 複製代碼
類裝飾器顧名思義,就是用來裝飾類的。它接收一個參數:
看完第一眼後,是否是感受都很差了。沒事,咱們立刻來個例子:
function Greeter(target: Function): void {
target.prototype.greet = function (): void {
console.log("Hello Semlinker!");
};
}
@Greeter
class Greeting {
constructor() {
// 內部實現
}
}
let myGreeting = new Greeting();
(myGreeting as any).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 as any).greet(); // console output: 'Hello TS!';
複製代碼
屬性裝飾器聲明:
declare type PropertyDecorator = (target:Object, propertyKey: string | symbol ) => void;
複製代碼
屬性裝飾器顧名思義,用來裝飾類的屬性。它接收兩個參數:
趁熱打鐵,立刻來個例子熱熱身:
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
複製代碼
方法裝飾器聲明:
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol, descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
複製代碼
方法裝飾器顧名思義,用來裝飾類的方法。它接收三個參數:
廢話很少說,直接上例子:
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);
複製代碼
以上代碼成功運行後,控制檯會輸出如下結果:
"wrapped function: before invoking runTask"
"runTask invoked, args: learn ts"
"wrapped function: after invoking runTask"
"result: finished"
複製代碼
下面咱們來介紹一下參數裝飾器。
參數裝飾器聲明:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number ) => void
複製代碼
參數裝飾器顧名思義,是用來裝飾函數參數,它接收三個參數:
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;
}
}
複製代碼
以上代碼成功運行後,控制檯會輸出如下結果:
"The parameter in position 0 at Greeter has been decorated"
複製代碼
TypeScript 4.0 帶來了不少新的特性,這裏咱們只簡單介紹其中的兩個新特性。
當 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];
}
}
複製代碼
從構造函數推斷類屬性的類型,該特性給咱們帶來了便利。但在使用過程當中,若是咱們無法保證對成員屬性都進行賦值,那麼該屬性可能會被認爲是 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];
}
}
}
複製代碼
在如下的示例中,咱們使用元組類型來聲明剩餘參數的類型:
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
函數,咱們也能夠這樣實現:
function addPerson(name: string, age: number) {
console.log(`Person info: name: ${name}, age: ${age}`)
}
複製代碼
這兩種方式看起來沒有多大的區別,但對於第一種方式,咱們無法設置第一個參數和第二個參數的名稱。雖然這樣對類型檢查沒有影響,但在元組位置上缺乏標籤,會使得它們難於使用。爲了提升開發者使用元組的體驗,TypeScript 4.0 支持爲元組類型設置標籤:
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]}`);
}
複製代碼
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 官方提供的在線 TypeScript 運行環境,利用它你能夠方便地學習 TypeScript 相關知識與不一樣版本的功能特性。
除了 TypeScript 官方的 Playground 以外,你還能夠選擇其餘的 Playground,好比 codepen.io、stackblitz 或 jsbin.com 等。
簡介:一款在線 TypeScript UML 工具,利用它你能夠爲指定的 TypeScript 代碼生成 UML 類圖。
簡介:一款 TypeScript 在線工具,利用它你能夠爲指定的 JSON 數據生成對應的 TypeScript 接口定義。
在線地址:www.jsontots.com/
除了使用 jsontots 在線工具以外,對於使用 VSCode IDE 的小夥們還能夠安裝 JSON to TS 擴展來快速完成 JSON to TS 的轉換工做。
簡介:利用 Schemats,你能夠基於(Postgres,MySQL)SQL 數據庫中的 schema 自動生成 TypeScript 接口定義。
簡介:一款 TypeScript AST 在線工具,利用它你能夠查看指定 TypeScript 代碼對應的 AST(Abstract Syntax Tree)抽象語法樹。
在線地址:ts-ast-viewer.com/
對於瞭解過 AST 的小夥伴來講,對 astexplorer 這款在線工具應該不會陌生。該工具除了支持 JavaScript 以外,還支持 CSS、JSON、RegExp、GraphQL 和 Markdown 等格式的解析。
簡介:TypeDoc 用於將 TypeScript 源代碼中的註釋轉換爲 HTML 文檔或 JSON 模型。它可靈活擴展,並支持多種配置。
在線地址:typedoc.org/
簡介:使用 TypeScript ESLint 能夠幫助咱們規範代碼質量,提升團隊開發效率。
對 TypeScript ESLint 項目感興趣且想在項目中應用的小夥伴,能夠參考 「在Typescript項目中,如何優雅的使用ESLint和Prettier」 這篇文章。
能堅持看到這裏的小夥伴都是 「真愛」,若是你還意猶未盡,那就來看看本人整理的 Github 上 1.8K+ 的開源項目:awesome-typescript。