一十偶、TypeScript 是什么
TypeScript 是一種由微軟開發(fā)的自由和開源的編程語言囚灼。它是 JavaScript 的一個超集摩疑,而且本質(zhì)上向這個語言添加了可選的靜態(tài)類型和基于類的面向?qū)ο缶幊獭?/p>
TypeScript 提供最新的和不斷發(fā)展的 JavaScript 特性,包括那些來自 2015 年的 ECMAScript 和未來的提案中的特性,比如異步功能和 Decorators,以幫助建立健壯的組件飘诗。下圖顯示了 TypeScript 與 ES5、ES2015 和 ES2016 之間的關(guān)系:
1.1 TypeScript 與 JavaScript 的區(qū)別
TypeScript | JavaScript |
---|---|
JavaScript 的超集用于解決大型項目的代碼復雜性 | 一種腳本語言界逛,用于創(chuàng)建動態(tài)網(wǎng)頁 |
可以在編譯期間發(fā)現(xiàn)并糾正錯誤 | 作為一種解釋型語言昆稿,只能在運行時發(fā)現(xiàn)錯誤 |
強類型,支持靜態(tài)和動態(tài)類型 | 弱類型息拜,沒有靜態(tài)類型選項 |
最終被編譯成 JavaScript 代碼溉潭,使瀏覽器可以理解 | 可以直接在瀏覽器中使用 |
支持模塊、泛型和接口 | 不支持模塊少欺,泛型或接口 |
社區(qū)的支持仍在增長喳瓣,而且還不是很大 | 大量的社區(qū)支持以及大量文檔和解決問題的支持 |
1.2 獲取 TypeScript
命令行的 TypeScript 編譯器可以使用 npm 包管理器來安裝。
1.安裝 TypeScript
$ npm install -g typescript
2.驗證 TypeScript
$ tsc -v
# Version 4.0.2
3.編譯 TypeScript 文件
$ tsc helloworld.ts
# helloworld.ts => helloworld.js
當然赞别,對剛?cè)腴T TypeScript 的小伙伴來說畏陕,也可以不用安裝 typescript
,而是直接使用線上的 TypeScript Playground 來學習新的語法或新特性仿滔。通過配置 TS Config 的 Target惠毁,可以設(shè)置不同的編譯目標,從而編譯生成不同的目標代碼崎页。
下圖示例中所設(shè)置的編譯目標是 ES5:
(圖片來源:https://www.typescriptlang.org/play)
1.3 典型 TypeScript 工作流程
如你所見鞠绰,在上圖中包含 3 個 ts 文件:a.ts、b.ts 和 c.ts飒焦。這些文件將被 TypeScript 編譯器蜈膨,根據(jù)配置的編譯選項編譯成 3 個 js 文件,即 a.js牺荠、b.js 和 c.js丈挟。對于大多數(shù)使用 TypeScript 開發(fā)的 Web 項目,我們還會對編譯生成的 js 文件進行打包處理志电,然后在進行部署曙咽。
1.4 TypeScript 初體驗
新建一個 hello.ts
文件,并輸入以下內(nèi)容:
function greet(person: string) {
return 'Hello, ' + person;
}
console.log(greet("TypeScript"));
然后執(zhí)行 tsc hello.ts
命令挑辆,之后會生成一個編譯好的文件 hello.js
:
"use strict";
function greet(person) {
return 'Hello, ' + person;
}
console.log(greet("TypeScript"));
觀察以上編譯后的輸出結(jié)果例朱,我們發(fā)現(xiàn) person
參數(shù)的類型信息在編譯后被擦除了。TypeScript 只會在編譯階段對類型進行靜態(tài)檢查鱼蝉,如果發(fā)現(xiàn)有錯誤洒嗤,編譯時就會報錯。而在運行時魁亦,編譯生成的 JS 與普通的 JavaScript 文件一樣渔隶,并不會進行類型檢查。
二、TypeScript 基礎(chǔ)類型
2.1 Boolean 類型
let isDone: boolean = false;
// ES5:var isDone = false;
2.2 Number 類型
let count: number = 10;
// ES5:var count = 10;
2.3 String 類型
let name: string = "semliker";
// ES5:var name = 'semlinker';
2.4 Symbol 類型
const sym = Symbol();
let obj = {
[sym]: "semlinker",
};
console.log(obj[sym]); // semlinker
2.5 Array 類型
let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];
let list: Array<number> = [1, 2, 3]; // Array<number>泛型語法
// ES5:var list = [1,2,3];
2.6 Enum 類型
使用枚舉我們可以定義一些帶名字的常量间唉。 使用枚舉可以清晰地表達意圖或創(chuàng)建一組有區(qū)別的用例绞灼。 TypeScript 支持數(shù)字的和基于字符串的枚舉。
1.數(shù)字枚舉
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
默認情況下,NORTH 的初始值為 0,其余的成員會從 1 開始自動增長数尿。換句話說,Direction.SOUTH 的值為 1军掂,Direction.EAST 的值為 2,Direction.WEST 的值為 3昨悼。
以上的枚舉示例經(jīng)編譯后蝗锥,對應(yīng)的 ES5 代碼如下:
"use strict";
var Direction;
(function (Direction) {
Direction[(Direction["NORTH"] = 0)] = "NORTH";
Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
Direction[(Direction["EAST"] = 2)] = "EAST";
Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH;
當然我們也可以設(shè)置 NORTH 的初始值,比如:
enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST,
}
2.字符串枚舉
在 TypeScript 2.4 版本率触,允許我們使用字符串枚舉终议。在一個字符串枚舉里,每個成員都必須用字符串字面量闲延,或另外一個字符串枚舉成員進行初始化。
enum Direction {
NORTH = "NORTH",
SOUTH = "SOUTH",
EAST = "EAST",
WEST = "WEST",
}
以上代碼對應(yīng)的 ES5 代碼如下:
"use strict";
var Direction;
(function (Direction) {
Direction["NORTH"] = "NORTH";
Direction["SOUTH"] = "SOUTH";
Direction["EAST"] = "EAST";
Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));
通過觀察數(shù)字枚舉和字符串枚舉的編譯結(jié)果韩玩,我們可以知道數(shù)字枚舉除了支持 從成員名稱到成員值 的普通映射之外垒玲,它還支持 從成員值到成員名稱 的反向映射:
enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dirName = Direction[0]; // NORTH
let dirVal = Direction["NORTH"]; // 0
另外,對于純字符串枚舉找颓,我們不能省略任何初始化程序合愈。而數(shù)字枚舉如果沒有顯式設(shè)置值時,則會使用默認規(guī)則進行初始化击狮。
3.常量枚舉
除了數(shù)字枚舉和字符串枚舉之外佛析,還有一種特殊的枚舉 —— 常量枚舉。它是使用 const
關(guān)鍵字修飾的枚舉彪蓬,常量枚舉會使用內(nèi)聯(lián)語法寸莫,不會為枚舉類型編譯生成任何 JavaScript。為了更好地理解這句話档冬,我們來看一個具體的例子:
const enum Direction {
NORTH,
SOUTH,
EAST,
WEST,
}
let dir: Direction = Direction.NORTH;
以上代碼對應(yīng)的 ES5 代碼如下:
"use strict";
var dir = 0 /* NORTH */;
4.異構(gòu)枚舉
異構(gòu)枚舉的成員值是數(shù)字和字符串的混合:
enum Enum {
A,
B,
C = "C",
D = "D",
E = 8,
F,
}
以上代碼對于的 ES5 代碼如下:
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
Enum[Enum["B"] = 1] = "B";
Enum["C"] = "C";
Enum["D"] = "D";
Enum[Enum["E"] = 8] = "E";
Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {}));
通過觀察上述生成的 ES5 代碼膘茎,我們可以發(fā)現(xiàn)數(shù)字枚舉相對字符串枚舉多了 “反向映射”:
console.log(Enum.A) //輸出:0
console.log(Enum[0]) // 輸出:A
2.7 Any 類型
在 TypeScript 中,任何類型都可以被歸為 any 類型酷誓。這讓 any 類型成為了類型系統(tǒng)的頂級類型(也被稱作全局超級類型)披坏。
let notSure: any = 666;
notSure = "semlinker";
notSure = false;
any
類型本質(zhì)上是類型系統(tǒng)的一個逃逸艙。作為開發(fā)者盐数,這給了我們很大的自由:TypeScript 允許我們對 any
類型的值執(zhí)行任何操作棒拂,而無需事先執(zhí)行任何形式的檢查。比如:
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
在許多場景下玫氢,這太寬松了帚屉。使用 any
類型谜诫,可以很容易地編寫類型正確但在運行時有問題的代碼。如果我們使用 any
類型涮阔,就無法使用 TypeScript 提供的大量的保護機制猜绣。為了解決 any
帶來的問題,TypeScript 3.0 引入了 unknown
類型敬特。
2.8 Unknown 類型
就像所有類型都可以賦值給 any
掰邢,所有類型也都可以賦值給 unknown
。這使得 unknown
成為 TypeScript 類型系統(tǒng)的另一種頂級類型(另一種是 any
)伟阔。下面我們來看一下 unknown
類型的使用示例:
let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
對 value
變量的所有賦值都被認為是類型正確的辣之。但是,當我們嘗試將類型為 unknown
的值賦值給其他類型的變量時會發(fā)生什么皱炉?
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
unknown
類型只能被賦值給 any
類型和 unknown
類型本身怀估。直觀地說,這是有道理的:只有能夠保存任意類型值的容器才能保存 unknown
類型的值合搅。畢竟我們不知道變量 value
中存儲了什么類型的值多搀。
現(xiàn)在讓我們看看當我們嘗試對類型為 unknown
的值執(zhí)行操作時會發(fā)生什么。以下是我們在之前 any
章節(jié)看過的相同操作:
let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
將 value
變量類型設(shè)置為 unknown
后灾部,這些操作都不再被認為是類型正確的康铭。通過將 any
類型改變?yōu)?unknown
類型,我們已將允許所有更改的默認設(shè)置赌髓,更改為禁止任何更改从藤。
2.9 Tuple 類型
眾所周知,數(shù)組一般由同種類型的值組成锁蠕,但有時我們需要在單個變量中存儲不同類型的值夷野,這時候我們就可以使用元組。在 JavaScript 中是沒有元組的荣倾,元組是 TypeScript 中特有的類型悯搔,其工作方式類似于數(shù)組。
元組可用于定義具有有限數(shù)量的未命名屬性的類型舌仍。每個屬性都有一個關(guān)聯(lián)的類型鳖孤。使用元組時,必須提供每個屬性的值抡笼。為了更直觀地理解元組的概念苏揣,我們來看一個具體的例子:
let tupleType: [string, boolean];
tupleType = ["semlinker", true];
在上面代碼中,我們定義了一個名為 tupleType
的變量推姻,它的類型是一個類型數(shù)組 [string, boolean]
平匈,然后我們按照正確的類型依次初始化 tupleType 變量。與數(shù)組一樣,我們可以通過下標來訪問元組中的元素:
console.log(tupleType[0]); // semlinker
console.log(tupleType[1]); // true
在元組初始化的時候增炭,如果出現(xiàn)類型不匹配的話忍燥,比如:
tupleType = [true, "semlinker"];
此時,TypeScript 編譯器會提示以下錯誤信息:
[0]: Type 'true' is not assignable to type 'string'.
[1]: Type 'string' is not assignable to type 'boolean'.
很明顯是因為類型不匹配導致的隙姿。在元組初始化的時候梅垄,我們還必須提供每個屬性的值,不然也會出現(xiàn)錯誤输玷,比如:
tupleType = ["semlinker"];
此時队丝,TypeScript 編譯器會提示以下錯誤信息:
Property '1' is missing in type '[string]' but required in type '[string, boolean]'.
2.10 Void 類型
某種程度上來說,void 類型像是與 any 類型相反欲鹏,它表示沒有任何類型机久。當一個函數(shù)沒有返回值時,你通常會見到其返回值類型是 void:
// 聲明函數(shù)返回值為void
function warnUser(): void {
console.log("This is my warning message");
}
以上代碼編譯生成的 ES5 代碼如下:
"use strict";
function warnUser() {
console.log("This is my warning message");
}
需要注意的是赔嚎,聲明一個 void 類型的變量沒有什么作用膘盖,因為在嚴格模式下,它的值只能為 undefined
:
let unusable: void = undefined;
2.11 Null 和 Undefined 類型
TypeScript 里尤误,undefined
和 null
兩者有各自的類型分別為 undefined
和 null
侠畔。
let u: undefined = undefined;
let n: null = null;
2.12 object, Object 和 {} 類型
1.object 類型
object 類型是:TypeScript 2.2 引入的新類型,它用于表示非原始類型损晤。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
create(o: object | null): any;
// ...
}
const proto = {};
Object.create(proto); // OK
Object.create(null); // OK
Object.create(undefined); // Error
Object.create(1337); // Error
Object.create(true); // Error
Object.create("oops"); // Error
2.Object 類型
Object 類型:它是所有 Object 類的實例的類型软棺,它由以下兩個接口來定義:
- Object 接口定義了 Object.prototype 原型對象上的屬性;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
constructor: Function;
toString(): string;
toLocaleString(): string;
valueOf(): Object;
hasOwnProperty(v: PropertyKey): boolean;
isPrototypeOf(v: Object): boolean;
propertyIsEnumerable(v: PropertyKey): boolean;
}
- ObjectConstructor 接口定義了 Object 類的屬性沉馆。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
/** Invocation via `new` */
new(value?: any): Object;
/** Invocation via function calls */
(value?: any): any;
readonly prototype: Object;
getPrototypeOf(o: any): any;
// ···
}
declare var Object: ObjectConstructor;
Object 類的所有實例都繼承了 Object 接口中的所有屬性码党。
3.{} 類型
{} 類型描述了一個沒有成員的對象德崭。當你試圖訪問這樣一個對象的任意屬性時斥黑,TypeScript 會產(chǎn)生一個編譯時錯誤。
// Type {}
const obj = {};
// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";
但是眉厨,你仍然可以使用在 Object 類型上定義的所有屬性和方法锌奴,這些屬性和方法可通過 JavaScript 的原型鏈隱式地使用:
// Type {}
const obj = {};
// "[object Object]"
obj.toString();
2.13 Never 類型
never
類型表示的是那些永不存在的值的類型。 例如憾股,never
類型是那些總是會拋出異陈故瘢或根本就不會有返回值的函數(shù)表達式或箭頭函數(shù)表達式的返回值類型。
// 返回never的函數(shù)必須存在無法達到的終點
function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
在 TypeScript 中服球,可以利用 never 類型的特性來實現(xiàn)全面性檢查茴恰,具體示例如下:
type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === "string") {
// 這里 foo 被收窄為 string 類型
} else if (typeof foo === "number") {
// 這里 foo 被收窄為 number 類型
} else {
// foo 在這里是 never
const check: never = foo;
}
}
注意在 else 分支里面,我們把收窄為 never 的 foo 賦值給一個顯示聲明的 never 變量斩熊。如果一切邏輯正確往枣,那么這里應(yīng)該能夠編譯通過。但是假如后來有一天你的同事修改了 Foo 的類型:
type Foo = string | number | boolean;
然而他忘記同時修改 controlFlowAnalysisWithNever
方法中的控制流程,這時候 else 分支的 foo 類型會被收窄為 boolean
類型分冈,導致無法賦值給 never 類型圾另,這時就會產(chǎn)生一個編譯錯誤。通過這個方式雕沉,我們可以確保
controlFlowAnalysisWithNever
方法總是窮盡了 Foo 的所有可能類型集乔。 通過這個示例,我們可以得出一個結(jié)論:使用 never 避免出現(xiàn)新增了聯(lián)合類型沒有對應(yīng)的實現(xiàn)坡椒,目的就是寫出類型絕對安全的代碼扰路。
三、TypeScript 斷言
3.1 類型斷言
有時候你會遇到這樣的情況肠牲,你會比 TypeScript 更了解某個值的詳細信息幼衰。通常這會發(fā)生在你清楚地知道一個實體具有比它現(xiàn)有類型更確切的類型。
通過類型斷言這種方式可以告訴編譯器缀雳,“相信我渡嚣,我知道自己在干什么”。類型斷言好比其他語言里的類型轉(zhuǎn)換肥印,但是不進行特殊的數(shù)據(jù)檢查和解構(gòu)识椰。它沒有運行時的影響,只是在編譯階段起作用深碱。
類型斷言有兩種形式:
1.“尖括號” 語法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
2.as 語法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
3.2 非空斷言
在上下文中當類型檢查器無法斷定類型時腹鹉,一個新的后綴表達式操作符 !
可以用于斷言操作對象是非 null 和非 undefined 類型。具體而言敷硅,x! 將從 x 值域中排除 null 和 undefined 功咒。
那么非空斷言操作符到底有什么用呢?下面我們先來看一下非空斷言操作符的一些使用場景绞蹦。
1.忽略 undefined 和 null 類型
function myFunc(maybeString: string | undefined | null) {
// Type 'string | null | undefined' is not assignable to type 'string'.
// Type 'undefined' is not assignable to type 'string'.
const onlyString: string = maybeString; // Error
const ignoreUndefinedAndNull: string = maybeString!; // Ok
}
2.調(diào)用函數(shù)時忽略 undefined 類型
type NumGenerator = () => number;
function myFunc(numGenerator: NumGenerator | undefined) {
// Object is possibly 'undefined'.(2532)
// Cannot invoke an object which is possibly 'undefined'.(2722)
const num1 = numGenerator(); // Error
const num2 = numGenerator!(); //OK
}
因為 !
非空斷言操作符會從編譯生成的 JavaScript 代碼中移除力奋,所以在實際使用的過程中,要特別注意幽七。比如下面這個例子:
const a: number | undefined = undefined;
const b: number = a!;
console.log(b);
以上 TS 代碼會編譯生成以下 ES5 代碼:
"use strict";
const a = undefined;
const b = a;
console.log(b);
雖然在 TS 代碼中景殷,我們使用了非空斷言,使得 const b: number = a!;
語句可以通過 TypeScript 類型檢查器的檢查澡屡。但在生成的 ES5 代碼中猿挚,!
非空斷言操作符被移除了,所以在瀏覽器中執(zhí)行以上代碼驶鹉,在控制臺會輸出 undefined
绩蜻。
3.3 確定賦值斷言
在 TypeScript 2.7 版本中引入了確定賦值斷言,即允許在實例屬性和變量聲明后面放置一個 !
號室埋,從而告訴 TypeScript 該屬性會被明確地賦值办绝。為了更好地理解它的作用踏兜,我們來看個具體的例子:
let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
x = 10;
}
很明顯該異常信息是說變量 x 在賦值前被使用了,要解決該問題八秃,我們可以使用確定賦值斷言:
let x!: number;
initialize();
console.log(2 * x); // Ok
function initialize() {
x = 10;
}
通過 let x!: number;
確定賦值斷言碱妆,TypeScript 編譯器就會知道該屬性會被明確地賦值。
四昔驱、類型守衛(wèi)
類型保護是可執(zhí)行運行時檢查的一種表達式疹尾,用于確保該類型在一定的范圍內(nèi)。 換句話說骤肛,類型保護可以保證一個字符串是一個字符串纳本,盡管它的值也可以是一個數(shù)值。類型保護與特性檢測并不是完全不同腋颠,其主要思想是嘗試檢測屬性繁成、方法或原型,以確定如何處理值淑玫。目前主要有四種的方式來實現(xiàn)類型保護:
4.1 in 關(guān)鍵字
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log("Name: " + emp.name);
if ("privileges" in emp) {
console.log("Privileges: " + emp.privileges);
}
if ("startDate" in emp) {
console.log("Start Date: " + emp.startDate);
}
}
4.2 typeof 關(guān)鍵字
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof
類型保護只支持兩種形式:typeof v === "typename"
和 typeof v !== typename
巾腕,"typename"
必須是 "number"
, "string"
絮蒿, "boolean"
或 "symbol"
尊搬。 但是 TypeScript 并不會阻止你與其它字符串比較,語言不會把那些表達式識別為類型保護土涝。
4.3 instanceof 關(guān)鍵字
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
// padder的類型收窄為 'SpaceRepeatingPadder'
}
4.4 自定義類型保護的類型謂詞
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
五佛寿、聯(lián)合類型和類型別名
5.1 聯(lián)合類型
聯(lián)合類型通常與 null
或 undefined
一起使用:
const sayHello = (name: string | undefined) => {
/* ... */
};
例如,這里 name
的類型是 string | undefined
意味著可以將 string
或 undefined
的值傳遞給sayHello
函數(shù)但壮。
sayHello("semlinker");
sayHello(undefined);
通過這個示例冀泻,你可以憑直覺知道類型 A 和類型 B 聯(lián)合后的類型是同時接受 A 和 B 值的類型。此外蜡饵,對于聯(lián)合類型來說弹渔,你可能會遇到以下的用法:
let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';
以上示例中的 1
、2
或 'click'
被稱為字面量類型验残,用來約束取值只能是某幾個值中的一個捞附。
5.2 可辨識聯(lián)合
TypeScript 可辨識聯(lián)合(Discriminated Unions)類型巾乳,也稱為代數(shù)數(shù)據(jù)類型或標簽聯(lián)合類型您没。它包含 3 個要點:可辨識、聯(lián)合類型和類型守衛(wèi)胆绊。
這種類型的本質(zhì)是結(jié)合聯(lián)合類型和字面量類型的一種類型保護方法氨鹏。如果一個類型是多個類型的聯(lián)合類型,且多個類型含有一個公共屬性压状,那么就可以利用這個公共屬性仆抵,來創(chuàng)建不同的類型保護區(qū)塊跟继。
1.可辨識
可辨識要求聯(lián)合類型中的每個元素都含有一個單例類型屬性,比如:
enum CarTransmission {
Automatic = 200,
Manual = 300
}
interface Motorcycle {
vType: "motorcycle"; // discriminant
make: number; // year
}
interface Car {
vType: "car"; // discriminant
transmission: CarTransmission
}
interface Truck {
vType: "truck"; // discriminant
capacity: number; // in tons
}
在上述代碼中镣丑,我們分別定義了 Motorcycle
舔糖、 Car
和 Truck
三個接口,在這些接口中都包含一個 vType
屬性莺匠,該屬性被稱為可辨識的屬性金吗,而其它的屬性只跟特性的接口相關(guān)。
2.聯(lián)合類型
基于前面定義了三個接口趣竣,我們可以創(chuàng)建一個 Vehicle
聯(lián)合類型:
type Vehicle = Motorcycle | Car | Truck;
現(xiàn)在我們就可以開始使用 Vehicle
聯(lián)合類型摇庙,對于 Vehicle
類型的變量,它可以表示不同類型的車輛遥缕。
3.類型守衛(wèi)
下面我們來定義一個 evaluatePrice
方法卫袒,該方法用于根據(jù)車輛的類型、容量和評估因子來計算價格单匣,具體實現(xiàn)如下:
const EVALUATION_FACTOR = Math.PI;
function evaluatePrice(vehicle: Vehicle) {
return vehicle.capacity * EVALUATION_FACTOR;
}
const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);
對于以上代碼夕凝,TypeScript 編譯器將會提示以下錯誤信息:
Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.
原因是在 Motorcycle 接口中,并不存在 capacity
屬性户秤,而對于 Car 接口來說迹冤,它也不存在 capacity
屬性。那么虎忌,現(xiàn)在我們應(yīng)該如何解決以上問題呢泡徙?這時,我們可以使用類型守衛(wèi)膜蠢。下面我們來重構(gòu)一下前面定義的 evaluatePrice
方法堪藐,重構(gòu)后的代碼如下:
function evaluatePrice(vehicle: Vehicle) {
switch(vehicle.vType) {
case "car":
return vehicle.transmission * EVALUATION_FACTOR;
case "truck":
return vehicle.capacity * EVALUATION_FACTOR;
case "motorcycle":
return vehicle.make * EVALUATION_FACTOR;
}
}
在以上代碼中,我們使用 switch
和 case
運算符來實現(xiàn)類型守衛(wèi)挑围,從而確保在 evaluatePrice
方法中礁竞,我們可以安全地訪問 vehicle
對象中的所包含的屬性,來正確的計算該車輛類型所對應(yīng)的價格杉辙。
5.3 類型別名
類型別名用來給一個類型起個新名字模捂。
type Message = string | string[];
let greet = (message: Message) => {
// ...
};
六、交叉類型
在 TypeScript 中交叉類型是將多個類型合并為一個類型蜘矢。通過 &
運算符可以將現(xiàn)有的多種類型疊加到一起成為一種類型狂男,它包含了所需的所有類型的特性。
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
let point: Point = {
x: 1,
y: 1
}
在上面代碼中我們先定義了 PartialPointX
類型品腹,接著使用 &
運算符創(chuàng)建一個新的 Point
類型岖食,表示一個含有 x 和 y 坐標的點,然后定義了一個 Point
類型的變量并初始化舞吭。
6.1 同名基礎(chǔ)類型屬性的合并
那么現(xiàn)在問題來了泡垃,假設(shè)在合并多個類型的過程中析珊,剛好出現(xiàn)某些類型存在相同的成員,但對應(yīng)的類型又不一致蔑穴,比如:
interface X {
c: string;
d: string;
}
interface Y {
c: number;
e: string
}
type XY = X & Y;
type YX = Y & X;
let p: XY;
let q: YX;
在上面的代碼中忠寻,接口 X 和接口 Y 都含有一個相同的成員 c,但它們的類型不一致存和。對于這種情況锡溯,此時 XY 類型或 YX 類型中成員 c 的類型是不是可以是 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
。
6.2 同名非基礎(chǔ)類型屬性的合并
在上面示例中绞佩,剛好接口 X 和接口 Y 中內(nèi)部成員 c 的類型都是基本數(shù)據(jù)類型寺鸥,那么如果是非基本數(shù)據(jù)類型的話,又會是什么情形品山。我們來看個具體的例子:
interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }
interface A { x: D; }
interface B { x: E; }
interface C { x: F; }
type ABC = A & B & C;
let abc: ABC = {
x: {
d: true,
e: 'semlinker',
f: 666
}
};
console.log('abc:', abc);
以上代碼成功運行后胆建,控制臺會輸出以下結(jié)果:
由上圖可知,在混入多個類型時肘交,若存在相同的成員笆载,且成員類型為非基本數(shù)據(jù)類型,那么是可以成功合并涯呻。
七凉驻、TypeScript 函數(shù)
7.1 TypeScript 函數(shù)與 JavaScript 函數(shù)的區(qū)別
TypeScript | JavaScript |
---|---|
含有類型 | 無類型 |
箭頭函數(shù) | 箭頭函數(shù)(ES2015) |
函數(shù)類型 | 無函數(shù)類型 |
必填和可選參數(shù) | 所有參數(shù)都是可選的 |
默認參數(shù) | 默認參數(shù) |
剩余參數(shù) | 剩余參數(shù) |
函數(shù)重載 | 無函數(shù)重載 |
7.2 箭頭函數(shù)
1.常見語法
myBooks.forEach(() => console.log('reading'));
myBooks.forEach(title => console.log(title));
myBooks.forEach((title, idx, arr) =>
console.log(idx + '-' + title);
);
myBooks.forEach((title, idx, arr) => {
console.log(idx + '-' + title);
});
2.使用示例
// 未使用箭頭函數(shù)
function Book() {
let self = this;
self.publishDate = 2016;
setInterval(function () {
console.log(self.publishDate);
}, 1000);
}
// 使用箭頭函數(shù)
function Book() {
this.publishDate = 2016;
setInterval(() => {
console.log(this.publishDate);
}, 1000);
}
7.3 參數(shù)類型和返回類型
function createUserId(name: string, id: number): string {
return name + id;
}
7.4 函數(shù)類型
let IdGenerator: (chars: string, nums: number) => string;
function createUserId(name: string, id: number): string {
return name + id;
}
IdGenerator = createUserId;
7.5 可選參數(shù)及默認參數(shù)
// 可選參數(shù)
function createUserId(name: string, id: number, age?: number): string {
return name + id;
}
// 默認參數(shù)
function createUserId(
name: string = "semlinker",
id: number,
age?: number
): string {
return name + id;
}
在聲明函數(shù)時,可以通過 ?
號來定義可選參數(shù)复罐,比如 age?: number
這種形式涝登。在實際使用時,需要注意的是可選參數(shù)要放在普通參數(shù)的后面效诅,不然會導致編譯錯誤胀滚。
7.6 剩余參數(shù)
function push(array, ...items) {
items.forEach(function (item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
7.7 函數(shù)重載
函數(shù)重載或方法重載是使用相同名稱和不同參數(shù)數(shù)量或類型創(chuàng)建多個方法的一種能力。
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
// type Combinable = string | number;
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
在以上代碼中乱投,我們?yōu)?add 函數(shù)提供了多個函數(shù)類型定義咽笼,從而實現(xiàn)函數(shù)的重載。在 TypeScript 中除了可以重載普通函數(shù)之外篡腌,我們還可以重載類中的成員方法褐荷。
方法重載是指在同一個類中方法同名,參數(shù)不同(參數(shù)類型不同、參數(shù)個數(shù)不同或參數(shù)個數(shù)相同時參數(shù)的先后順序不同)解恰,調(diào)用時根據(jù)實參的形式隔盛,選擇與它匹配的方法執(zhí)行操作的一種技術(shù)珊泳。所以類中成員方法滿足重載的條件是:在同一個類中秘遏,方法名相同且參數(shù)列表不同传藏。下面我們來舉一個成員方法重載的例子:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');
這里需要注意的是乾忱,當 TypeScript 編譯器處理函數(shù)重載時限匣,它會查找重載列表抖苦,嘗試使用第一個重載定義。 如果匹配的話就使用這個米死。 因此锌历,在定義重載的時候,一定要把最精確的定義放在最前面峦筒。另外在 Calculator 類中究西,add(a: Combinable, b: Combinable){ }
并不是重載列表的一部分,因此對于 add 成員方法來說物喷,我們只定義了四個重載方法卤材。
八、TypeScript 數(shù)組
8.1 數(shù)組解構(gòu)
let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;
8.2 數(shù)組展開運算符
let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];
8.3 數(shù)組遍歷
let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
console.log(i);
}
九峦失、TypeScript 對象
9.1 對象解構(gòu)
let person = {
name: "Semlinker",
gender: "Male",
};
let { name, gender } = person;
9.2 對象展開運算符
let person = {
name: "Semlinker",
gender: "Male",
address: "Xiamen",
};
// 組裝對象
let personWithAge = { ...person, age: 33 };
// 獲取除了某些項外的其它項
let { name, ...rest } = person;
十扇丛、TypeScript 接口
在面向?qū)ο笳Z言中,接口是一個很重要的概念尉辑,它是對行為的抽象帆精,而具體如何行動需要由類去實現(xiàn)。
TypeScript 中的接口是一個非常靈活的概念隧魄,除了可用于對類的一部分行為進行抽象以外实幕,也常用于對「對象的形狀(Shape)」進行描述。
10.1 對象的形狀
interface Person {
name: string;
age: number;
}
let semlinker: Person = {
name: "semlinker",
age: 33,
};
10.2 可選 | 只讀屬性
interface Person {
readonly name: string;
age?: number;
}
只讀屬性用于限制只能在對象剛剛創(chuàng)建的時候修改其值堤器。此外 TypeScript 還提供了 ReadonlyArray<T>
類型昆庇,它與 Array<T>
相似,只是把所有可變方法去掉了闸溃,因此可以確保數(shù)組創(chuàng)建后再也不能被修改整吆。
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
10.3 任意屬性
有時候我們希望一個接口中除了包含必選和可選屬性之外,還允許有其他的任意屬性辉川,這時我們可以使用 索引簽名 的形式來滿足上述要求表蝙。
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
const p1 = { name: "semlinker" };
const p2 = { name: "lolo", age: 5 };
const p3 = { name: "kakuqo", sex: 1 }
10.4 接口與類型別名的區(qū)別
1.Objects/Functions
接口和類型別名都可以用來描述對象的形狀或函數(shù)簽名:
接口
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
類型別名
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
2.Other Types
與接口類型不一樣,類型別名可以用于一些其他類型乓旗,比如原始類型府蛇、聯(lián)合類型和元組:
// primitive
type Name = string;
// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
3.Extend
接口和類型別名都能夠被擴展,但語法有所不同屿愚。此外汇跨,接口和類型別名不是互斥的务荆。接口可以擴展類型別名,而反過來是不行的穷遂。
Interface extends interface
interface PartialPointX { x: number; }
interface Point extends PartialPointX {
y: number;
}
Type alias extends type alias
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
Interface extends type alias
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
Type alias extends interface
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
4.Implements
類可以以相同的方式實現(xiàn)接口或類型別名函匕,但類不能實現(xiàn)使用類型別名定義的聯(lián)合類型:
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x = 1;
y = 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x = 1;
y = 2;
}
type PartialPoint = { x: number; } | { y: number; };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
x = 1;
y = 2;
}
5.Declaration merging
與類型別名不同,接口可以定義多次蚪黑,會被自動合并為單個接口盅惜。
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };
十一、TypeScript 類
11.1 類的屬性與方法
在面向?qū)ο笳Z言中忌穿,類是一種面向?qū)ο笥嬎銠C編程語言的構(gòu)造抒寂,是創(chuàng)建對象的藍圖,描述了所創(chuàng)建的對象共同的屬性和方法掠剑。
在 TypeScript 中蓬推,我們可以通過 Class
關(guān)鍵字來定義一個類:
class Greeter {
// 靜態(tài)屬性
static cname: string = "Greeter";
// 成員屬性
greeting: string;
// 構(gòu)造函數(shù) - 執(zhí)行初始化操作
constructor(message: string) {
this.greeting = message;
}
// 靜態(tài)方法
static getClassName() {
return "Class name is Greeter";
}
// 成員方法
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
那么成員屬性與靜態(tài)屬性,成員方法與靜態(tài)方法有什么區(qū)別呢澡腾?這里無需過多解釋沸伏,我們直接看一下編譯生成的 ES5 代碼:
"use strict";
var Greeter = /** @class */ (function () {
// 構(gòu)造函數(shù) - 執(zhí)行初始化操作
function Greeter(message) {
this.greeting = message;
}
// 靜態(tài)方法
Greeter.getClassName = function () {
return "Class name is Greeter";
};
// 成員方法
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
// 靜態(tài)屬性
Greeter.cname = "Greeter";
return Greeter;
}());
var greeter = new Greeter("world");
11.2 ECMAScript 私有字段
在 TypeScript 3.8 版本就開始支持ECMAScript 私有字段,使用方式如下:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let semlinker = new Person("Semlinker");
semlinker.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
與常規(guī)屬性(甚至使用 private
修飾符聲明的屬性)不同动分,私有字段要牢記以下規(guī)則:
- 私有字段以
#
字符開頭毅糟,有時我們稱之為私有名稱; - 每個私有字段名稱都唯一地限定于其包含的類澜公;
- 不能在私有字段上使用 TypeScript 可訪問性修飾符(如 public 或 private)姆另;
- 私有字段不能在包含的類之外訪問,甚至不能被檢測到坟乾。
11.3 訪問器
在 TypeScript 中迹辐,我們可以通過 getter
和 setter
方法來實現(xiàn)數(shù)據(jù)的封裝和有效性校驗,防止出現(xiàn)異常數(shù)據(jù)甚侣。
let passcode = "Hello TypeScript";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "Hello TypeScript") {
this._fullName = newName;
} else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
console.log(employee.fullName);
}
11.4 類的繼承
繼承(Inheritance)是一種聯(lián)結(jié)類與類的層次模型明吩。指的是一個類(稱為子類、子接口)繼承另外的一個類(稱為父類殷费、父接口)的功能印荔,并可以增加它自己的新功能的能力,繼承是類與類或者接口與接口之間最常見的關(guān)系详羡。
繼承是一種 is-a 關(guān)系:
在 TypeScript 中仍律,我們可以通過 extends
關(guān)鍵字來實現(xiàn)繼承:
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name); // 調(diào)用父類的構(gòu)造函數(shù)
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
sam.move();
11.5 抽象類
使用 abstract
關(guān)鍵字聲明的類,我們稱之為抽象類实柠。抽象類不能被實例化水泉,因為它里面包含一個或多個抽象方法。所謂的抽象方法,是指不包含具體實現(xiàn)的方法:
abstract class Person {
constructor(public name: string){}
abstract say(words: string) :void;
}
// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error
抽象類不能被直接實例化草则,我們只能實例化實現(xiàn)了所有抽象方法的子類钢拧。具體如下所示:
abstract class Person {
constructor(public name: string){}
// 抽象方法
abstract say(words: string) :void;
}
class Developer extends Person {
constructor(name: string) {
super(name);
}
say(words: string): void {
console.log(`${this.name} says ${words}`);
}
}
const lolo = new Developer("lolo");
lolo.say("I love ts!"); // lolo says I love ts!
11.6 類方法重載
在前面的章節(jié),我們已經(jīng)介紹了函數(shù)重載畔师。對于類的方法來說娶靡,它也支持重載牧牢。比如看锉,在以下示例中我們重載了 ProductService
類的 getProducts
成員方法:
class ProductService {
getProducts(): void;
getProducts(id: number): void;
getProducts(id?: number) {
if(typeof id === 'number') {
console.log(`獲取id為 ${id} 的產(chǎn)品信息`);
} else {
console.log(`獲取所有的產(chǎn)品信息`);
}
}
}
const productService = new ProductService();
productService.getProducts(666); // 獲取id為 666 的產(chǎn)品信息
productService.getProducts(); // 獲取所有的產(chǎn)品信息
十二、TypeScript 泛型
軟件工程中塔鳍,我們不僅要創(chuàng)建一致的定義良好的 API伯铣,同時也要考慮可重用性。 組件不僅能夠支持當前的數(shù)據(jù)類型轮纫,同時也能支持未來的數(shù)據(jù)類型腔寡,這在創(chuàng)建大型系統(tǒng)時為你提供了十分靈活的功能。
在像 C# 和 Java 這樣的語言中掌唾,可以使用泛型來創(chuàng)建可重用的組件放前,一個組件可以支持多種類型的數(shù)據(jù)。 這樣用戶就可以以自己的數(shù)據(jù)類型來使用組件糯彬。
設(shè)計泛型的關(guān)鍵目的是在成員之間提供有意義的約束凭语,這些成員可以是:類的實例成員、類的方法撩扒、函數(shù)參數(shù)和函數(shù)返回值似扔。
泛型(Generics)是允許同一個函數(shù)接受不同類型參數(shù)的一種模板。相比于使用 any 類型搓谆,使用泛型來創(chuàng)建可復用的組件要更好炒辉,因為泛型會保留參數(shù)類型。
12.1 泛型語法
對于剛接觸 TypeScript 泛型的讀者來說泉手,首次看到 <T>
語法會感到陌生黔寇。其實它沒有什么特別,就像傳遞參數(shù)一樣斩萌,我們傳遞了我們想要用于特定函數(shù)調(diào)用的類型啡氢。
參考上面的圖片,當我們調(diào)用 identity<Number>(1)
术裸,Number
類型就像參數(shù) 1
一樣倘是,它將在出現(xiàn) T
的任何位置填充該類型。圖中 <T>
內(nèi)部的 T
被稱為類型變量袭艺,它是我們希望傳遞給 identity 函數(shù)的類型占位符搀崭,同時它被分配給 value
參數(shù)用來代替它的類型:此時 T
充當?shù)氖穷愋停皇翘囟ǖ?Number 類型。
其中 T
代表 Type瘤睹,在定義泛型時通常用作第一個類型變量名稱升敲。但實際上 T
可以用任何有效名稱代替。除了 T
之外轰传,以下是常見泛型變量代表的意思:
- K(Key):表示對象中的鍵類型驴党;
- V(Value):表示對象中的值類型;
- E(Element):表示元素類型获茬。
其實并不是只能定義一個類型變量港庄,我們可以引入希望定義的任何數(shù)量的類型變量。比如我們引入一個新的類型變量 U
恕曲,用于擴展我們定義的 identity
函數(shù):
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
除了為類型變量顯式設(shè)定值之外鹏氧,一種更常見的做法是使編譯器自動選擇這些類型,從而使代碼更簡潔佩谣。我們可以完全省略尖括號把还,比如:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity(68, "Semlinker"));
對于上述代碼,編譯器足夠聰明茸俭,能夠知道我們的參數(shù)類型吊履,并將它們賦值給 T 和 U,而不需要開發(fā)人員顯式指定它們调鬓。
12.2 泛型接口
interface GenericIdentityFn<T> {
(arg: T): T;
}
12.3 泛型類
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
12.4 泛型工具類型
為了方便開發(fā)者 TypeScript 內(nèi)置了一些常用的工具類型艇炎,比如 Partial、Required袖迎、Readonly冕臭、Record 和 ReturnType 等。出于篇幅考慮燕锥,這里我們只簡單介紹 Partial 工具類型辜贵。不過在具體介紹之前,我們得先介紹一些相關(guān)的基礎(chǔ)知識归形,方便讀者自行學習其它的工具類型托慨。
1.typeof
在 TypeScript 中,typeof
操作符可以用來獲取一個變量聲明或?qū)ο蟮念愋汀?/p>
interface Person {
name: string;
age: number;
}
const sem: Person = { name: 'semlinker', age: 33 };
type Sem= typeof sem; // -> Person
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
2.keyof
keyof
操作符是在 TypeScript 2.1 版本引入的暇榴,該操作符可以用于獲取某種類型的所有鍵厚棵,其返回類型是聯(lián)合類型。
interface Person {
name: string;
age: number;
}
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number
在 TypeScript 中支持兩種索引簽名蔼紧,數(shù)字索引和字符串索引:
interface StringArray {
// 字符串索引 -> keyof StringArray => string | number
[index: string]: string;
}
interface StringArray1 {
// 數(shù)字索引 -> keyof StringArray1 => number
[index: number]: string;
}
為了同時支持兩種索引類型婆硬,就得要求數(shù)字索引的返回值必須是字符串索引返回值的子類。其中的原因就是當使用數(shù)值索引時奸例,JavaScript 在執(zhí)行索引操作時彬犯,會先把數(shù)值索引先轉(zhuǎn)換為字符串索引向楼。所以 keyof { [x: string]: Person }
的結(jié)果會返回 string | number
。
3.in
in
用來遍歷枚舉類型:
type Keys = "a" | "b" | "c"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any, c: any }
4.infer
在條件類型語句中谐区,可以用 infer
聲明一個類型變量并且對它進行使用湖蜕。
type ReturnType<T> = T extends (
...args: any[]
) => infer R ? R : any;
以上代碼中 infer R
就是聲明一個變量來承載傳入函數(shù)簽名的返回值類型,簡單說就是用它取到函數(shù)返回值的類型方便之后使用宋列。
5.extends
有時候我們定義的泛型不想過于靈活或者說想繼承某些類等昭抒,可以通過 extends 關(guān)鍵字添加泛型約束。
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
現(xiàn)在這個泛型函數(shù)被定義了約束炼杖,因此它不再是適用于任意類型:
loggingIdentity(3); // Error, number doesn't have a .length property
這時我們需要傳入符合約束類型的值灭返,必須包含必須的屬性:
loggingIdentity({length: 10, value: 3});
6.Partial
Partial<T>
的作用就是將某個類型里的屬性全部變?yōu)榭蛇x項 ?
。
定義:
/**
* node_modules/typescript/lib/lib.es5.d.ts
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
在以上代碼中嘹叫,首先通過 keyof T
拿到 T
的所有屬性名婆殿,然后使用 in
進行遍歷诈乒,將值賦給 P
罩扇,最后通過 T[P]
取得相應(yīng)的屬性值。中間的 ?
號怕磨,用于將所有屬性變?yōu)榭蛇x喂饥。
示例:
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "Learn TS",
description: "Learn TypeScript",
};
const todo2 = updateTodo(todo1, {
description: "Learn TypeScript Enum",
});
在上面的 updateTodo
方法中,我們利用 Partial<T>
工具類型肠鲫,定義 fieldsToUpdate
的類型為 Partial<Todo>
员帮,即:
{
title?: string | undefined;
description?: string | undefined;
}
十三、TypeScript 裝飾器
13.1 裝飾器是什么
- 它是一個表達式
- 該表達式被執(zhí)行后导饲,返回一個函數(shù)
- 函數(shù)的入?yún)⒎謩e為 target捞高、name 和 descriptor
- 執(zhí)行該函數(shù)后,可能返回 descriptor 對象渣锦,用于配置 target 對象
13.2 裝飾器的分類
- 類裝飾器(Class decorators)
- 屬性裝飾器(Property decorators)
- 方法裝飾器(Method decorators)
- 參數(shù)裝飾器(Parameter decorators)
需要注意的是硝岗,若要啟用實驗性的裝飾器特性,你必須在命令行或 tsconfig.json
里啟用 experimentalDecorators
編譯器選項:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
13.3 類裝飾器
類裝飾器聲明:
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
類裝飾器顧名思義袋毙,就是用來裝飾類的型檀。它接收一個參數(shù):
- target: TFunction - 被裝飾的類
看完第一眼后,是不是感覺都不好了听盖。沒事胀溺,我們馬上來個例子:
function Greeter(target: Function): void {
target.prototype.greet = function (): void {
console.log("Hello Semlinker!");
};
}
@Greeter
class Greeting {
constructor() {
// 內(nèi)部實現(xiàn)
}
}
let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello Semlinker!';
上面的例子中,我們定義了 Greeter
類裝飾器皆看,同時我們使用了 @Greeter
語法糖仓坞,來使用裝飾器。
友情提示:讀者可以直接復制上面的代碼腰吟,在 TypeScript Playground 中運行查看結(jié)果无埃。
有的讀者可能想問,例子中總是輸出 Hello Semlinker!
,能自定義輸出的問候語么 录语?這個問題很好倍啥,答案是可以的。
具體實現(xiàn)如下:
function Greeter(greeting: string) {
return function (target: Function) {
target.prototype.greet = function (): void {
console.log(greeting);
};
};
}
@Greeter("Hello TS!")
class Greeting {
constructor() {
// 內(nèi)部實現(xiàn)
}
}
let myGreeting = new Greeting();
(myGreeting as any).greet(); // console output: 'Hello TS!';
13.4 屬性裝飾器
屬性裝飾器聲明:
declare type PropertyDecorator = (target:Object,
propertyKey: string | symbol ) => void;
屬性裝飾器顧名思義澎埠,用來裝飾類的屬性虽缕。它接收兩個參數(shù):
- target: Object - 被裝飾的類
- propertyKey: string | symbol - 被裝飾類的屬性名
趁熱打鐵,馬上來個例子熱熱身:
function logProperty(target: any, key: string) {
delete target[key];
const backingField = "_" + key;
Object.defineProperty(target, backingField, {
writable: true,
enumerable: true,
configurable: true
});
// property getter
const getter = function (this: any) {
const currVal = this[backingField];
console.log(`Get: ${key} => ${currVal}`);
return currVal;
};
// property setter
const setter = function (this: any, newVal: any) {
console.log(`Set: ${key} => ${newVal}`);
this[backingField] = newVal;
};
// Create new property with getter and setter
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class Person {
@logProperty
public name: string;
constructor(name : string) {
this.name = name;
}
}
const p1 = new Person("semlinker");
p1.name = "kakuqo";
以上代碼我們定義了一個 logProperty
函數(shù)蒲稳,來跟蹤用戶對屬性的操作氮趋,當代碼成功運行后,在控制臺會輸出以下結(jié)果:
Set: name => semlinker
Set: name => kakuqo
13.5 方法裝飾器
方法裝飾器聲明:
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
方法裝飾器顧名思義江耀,用來裝飾類的方法剩胁。它接收三個參數(shù):
- target: Object - 被裝飾的類
- propertyKey: string | symbol - 方法名
- descriptor: TypePropertyDescript - 屬性描述符
廢話不多說,直接上例子:
function log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
let originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("wrapped function: before invoking " + propertyKey);
let result = originalMethod.apply(this, args);
console.log("wrapped function: after invoking " + propertyKey);
return result;
};
}
class Task {
@log
runTask(arg: any): any {
console.log("runTask invoked, args: " + arg);
return "finished";
}
}
let task = new Task();
let result = task.runTask("learn ts");
console.log("result: " + result);
以上代碼成功運行后祥国,控制臺會輸出以下結(jié)果:
"wrapped function: before invoking runTask"
"runTask invoked, args: learn ts"
"wrapped function: after invoking runTask"
"result: finished"
下面我們來介紹一下參數(shù)裝飾器昵观。
13.6 參數(shù)裝飾器
參數(shù)裝飾器聲明:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
parameterIndex: number ) => void
參數(shù)裝飾器顧名思義,是用來裝飾函數(shù)參數(shù)舌稀,它接收三個參數(shù):
- target: Object - 被裝飾的類
- propertyKey: string | symbol - 方法名
- parameterIndex: number - 方法中參數(shù)的索引值
function Log(target: Function, key: string, parameterIndex: number) {
let functionLogged = key || target.prototype.constructor.name;
console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
been decorated`);
}
class Greeter {
greeting: string;
constructor(@Log phrase: string) {
this.greeting = phrase;
}
}
以上代碼成功運行后啊犬,控制臺會輸出以下結(jié)果:
"The parameter in position 0 at Greeter has been decorated"
十四、TypeScript 4.0 新特性
TypeScript 4.0 帶來了很多新的特性壁查,這里我們只簡單介紹其中的兩個新特性觉至。
14.1 構(gòu)造函數(shù)的類屬性推斷
當 noImplicitAny
配置屬性被啟用之后,TypeScript 4.0 就可以使用控制流分析來確認類中的屬性類型:
class Person {
fullName; // (property) Person.fullName: string
firstName; // (property) Person.firstName: string
lastName; // (property) Person.lastName: string
constructor(fullName: string) {
this.fullName = fullName;
this.firstName = fullName.split(" ")[0];
this.lastName = fullName.split(" ")[1];
}
}
然而對于以上的代碼睡腿,如果在 TypeScript 4.0 以前的版本语御,比如在 3.9.2 版本下,編譯器會提示以下錯誤信息:
class Person {
// Member 'fullName' implicitly has an 'any' type.(7008)
fullName; // Error
firstName; // Error
lastName; // Error
constructor(fullName: string) {
this.fullName = fullName;
this.firstName = fullName.split(" ")[0];
this.lastName = fullName.split(" ")[1];
}
}
從構(gòu)造函數(shù)推斷類屬性的類型席怪,該特性給我們帶來了便利应闯。但在使用過程中,如果我們沒法保證對成員屬性都進行賦值何恶,那么該屬性可能會被認為是 undefined
孽锥。
class Person {
fullName; // (property) Person.fullName: string
firstName; // (property) Person.firstName: string | undefined
lastName; // (property) Person.lastName: string | undefined
constructor(fullName: string) {
this.fullName = fullName;
if(Math.random()){
this.firstName = fullName.split(" ")[0];
this.lastName = fullName.split(" ")[1];
}
}
}
14.2 標記的元組元素
在以下的示例中,我們使用元組類型來聲明剩余參數(shù)的類型:
function addPerson(...args: [string, number]): void {
console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
}
addPerson("lolo", 5); // Person info: name: lolo, age: 5
其實细层,對于上面的 addPerson
函數(shù)惜辑,我們也可以這樣實現(xiàn):
function addPerson(name: string, age: number) {
console.log(`Person info: name: ${name}, age: ${age}`)
}
這兩種方式看起來沒有多大的區(qū)別,但對于第一種方式疫赎,我們沒法設(shè)置第一個參數(shù)和第二個參數(shù)的名稱盛撑。雖然這樣對類型檢查沒有影響,但在元組位置上缺少標簽捧搞,會使得它們難于使用抵卫。為了提高開發(fā)者使用元組的體驗狮荔,TypeScript 4.0 支持為元組類型設(shè)置標簽:
function addPerson(...args: [name: string, age: number]): void {
console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}
之后,當我們使用 addPerson
方法時介粘,TypeScript 的智能提示就會變得更加友好殖氏。
// 未使用標簽的智能提示
// addPerson(args_0: string, args_1: number): void
function addPerson(...args: [string, number]): void {
console.log(`Person info: name: ${args[0]}, age: ${args[1]}`)
}
// 已使用標簽的智能提示
// addPerson(name: string, age: number): void
function addPerson(...args: [name: string, age: number]): void {
console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
}
十五、編譯上下文
15.1 tsconfig.json 的作用
- 用于標識 TypeScript 項目的根路徑姻采;
- 用于配置 TypeScript 編譯器雅采;
- 用于指定編譯的文件。
15.2 tsconfig.json 重要字段
- files - 設(shè)置要編譯的文件的名稱慨亲;
- include - 設(shè)置需要進行編譯的文件婚瓜,支持路徑模式匹配;
- exclude - 設(shè)置無需進行編譯的文件刑棵,支持路徑模式匹配巴刻;
- compilerOptions - 設(shè)置與編譯流程相關(guān)的選項。
15.3 compilerOptions 選項
compilerOptions 支持很多選項蛉签,常見的有 baseUrl
胡陪、 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, // 生成相應(yīng)的 '.d.ts' 文件
"sourceMap": true, // 生成相應(yīng)的 '.map' 文件
"outFile": "./", // 將輸出文件合并為一個文件
"outDir": "./", // 指定輸出目錄
"rootDir": "./", // 用來控制輸出目錄結(jié)構(gòu) --outDir.
"removeComments": true, // 刪除編譯后的所有的注釋
"noEmit": true, // 不生成輸出文件
"importHelpers": true, // 從 tslib 導入輔助工具函數(shù)
"isolatedModules": true, // 將每個文件做為單獨的模塊 (與 'ts.transpileModule' 類似).
/* 嚴格的類型檢查選項 */
"strict": true, // 啟用所有嚴格類型檢查選項
"noImplicitAny": true, // 在表達式和聲明上有隱含的 any類型時報錯
"strictNullChecks": true, // 啟用嚴格的 null 檢查
"noImplicitThis": true, // 當 this 表達式值為 any 類型的時候营曼,生成一個錯誤
"alwaysStrict": true, // 以嚴格模式檢查每個模塊乒验,并在每個文件里加入 'use strict'
/* 額外的檢查 */
"noUnusedLocals": true, // 有未使用的變量時,拋出錯誤
"noUnusedParameters": true, // 有未使用的參數(shù)時蒂阱,拋出錯誤
"noImplicitReturns": true, // 并不是所有函數(shù)里的代碼都有返回值時锻全,拋出錯誤
"noFallthroughCasesInSwitch": true, // 報告 switch 語句的 fallthrough 錯誤。(即录煤,不允許 switch 的 case 語句貫穿)
/* 模塊解析選項 */
"moduleResolution": "node", // 選擇模塊解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相對模塊名稱的基目錄
"paths": {}, // 模塊名到基于 baseUrl 的路徑映射的列表
"rootDirs": [], // 根文件夾列表鳄厌,其組合內(nèi)容表示項目運行時的結(jié)構(gòu)內(nèi)容
"typeRoots": [], // 包含類型聲明的文件列表
"types": [], // 需要包含的類型聲明文件名列表
"allowSyntheticDefaultImports": true, // 允許從沒有設(shè)置默認導出的模塊中默認導入。
/* Source Map Options */
"sourceRoot": "./", // 指定調(diào)試器應(yīng)該找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定調(diào)試器應(yīng)該找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成單個 soucemaps 文件妈踊,而不是將 sourcemaps 生成不同的文件
"inlineSources": true, // 將代碼與 sourcemaps 生成到一個文件中了嚎,要求同時設(shè)置了 --inlineSourceMap 或 --sourceMap 屬性
/* 其他選項 */
"experimentalDecorators": true, // 啟用裝飾器
"emitDecoratorMetadata": true // 為裝飾器提供元數(shù)據(jù)的支持
}
}
十六、TypeScript 開發(fā)輔助工具
16.1 TypeScript Playground
簡介:TypeScript 官方提供的在線 TypeScript 運行環(huán)境廊营,利用它你可以方便地學習 TypeScript 相關(guān)知識與不同版本的功能特性歪泳。
除了 TypeScript 官方的 Playground 之外,你還可以選擇其他的 Playground露筒,比如 codepen.io呐伞、stackblitz 或 jsbin.com 等。
16.2 TypeScript UML Playground
簡介:一款在線 TypeScript UML 工具慎式,利用它你可以為指定的 TypeScript 代碼生成 UML 類圖伶氢。
16.3 JSON TO TS
簡介:一款 TypeScript 在線工具趟径,利用它你可以為指定的 JSON 數(shù)據(jù)生成對應(yīng)的 TypeScript 接口定義。
除了使用 jsontots 在線工具之外癣防,對于使用 VSCode IDE 的小伙們還可以安裝 JSON to TS 擴展來快速完成 JSON to TS 的轉(zhuǎn)換工作蜗巧。
16.4 Schemats
簡介:利用 Schemats,你可以基于(Postgres蕾盯,MySQL)SQL 數(shù)據(jù)庫中的 schema 自動生成 TypeScript 接口定義惧蛹。
16.5 TypeScript AST Viewer
簡介:一款 TypeScript AST 在線工具,利用它你可以查看指定 TypeScript 代碼對應(yīng)的 AST(Abstract Syntax Tree)抽象語法樹刑枝。
對于了解過 AST 的小伙伴來說香嗓,對 astexplorer 這款在線工具應(yīng)該不會陌生。該工具除了支持 JavaScript 之外装畅,還支持 CSS靠娱、JSON、RegExp掠兄、GraphQL 和 Markdown 等格式的解析像云。
16.6 TypeDoc
簡介:TypeDoc 用于將 TypeScript 源代碼中的注釋轉(zhuǎn)換為 HTML 文檔或 JSON 模型。它可靈活擴展蚂夕,并支持多種配置迅诬。
在線地址:https://typedoc.org/
16.7 TypeScript ESLint
簡介:使用 TypeScript ESLint 可以幫助我們規(guī)范代碼質(zhì)量,提高團隊開發(fā)效率婿牍。
對 TypeScript ESLint 項目感興趣且想在項目中應(yīng)用的小伙伴侈贷,可以參考 “在Typescript項目中,如何優(yōu)雅的使用ESLint和Prettier” 這篇文章等脂。
能堅持看到這里的小伙伴都是 “真愛”俏蛮,如果你還意猶未盡,那就來看看本人整理的 Github 上 1.8K+ 的開源項目:awesome-typescript上遥。
十七搏屑、參考資源
- mariusschulz - the-unknown-type-in-typescript
- 深入理解 TypeScript - 編譯上下文
- TypeScript 4.0
- TypeScript Quickly