【TS】825- 了不起的 TypeScript 入門教程
創(chuàng)建了一個 “重學(xué)TypeScript” 的微信群,想加群的小伙伴,加我微信?"semlinker",備注重學(xué)TS。已出 TS 系列文章?39?篇。
想學(xué)習(xí) TypeScript 的小伙伴看過來,本文將帶你一步步學(xué)習(xí) TypeScript 入門相關(guān)的十四個知識點,詳細(xì)的內(nèi)容大綱請看下圖:

一、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 的超集用于解決大型項目的代碼復(fù)雜性 | 一種腳本語言,用于創(chuàng)建動態(tài)網(wǎng)頁。 |
| 可以在編譯期間發(fā)現(xiàn)并糾正錯誤 | 作為一種解釋型語言,只能在運行時發(fā)現(xiàn)錯誤 |
| 強類型,支持靜態(tài)和動態(tài)類型 | 弱類型,沒有靜態(tài)類型選項 |
| 最終被編譯成 JavaScript 代碼,使瀏覽器可以理解 | 可以直接在瀏覽器中使用 |
| 支持模塊、泛型和接口 | 不支持模塊,泛型或接口 |
| 支持 ES3,ES4,ES5 和 ES6 等 | 不支持編譯其他 ES3,ES4,ES5 或 ES6 功能 |
| 社區(qū)的支持仍在增長,而且還不是很大 | 大量的社區(qū)支持以及大量文檔和解決問題的支持 |
1.2 獲取 TypeScript
命令行的 TypeScript 編譯器可以使用 Node.js 包來安裝。
1.安裝 TypeScript
$?npm?install?-g?typescript
2.編譯 TypeScript 文件
$?tsc?helloworld.ts
#?helloworld.ts?=>?helloworld.js
當(dāng)然,對于剛?cè)腴T TypeScript 的小伙伴,也可以不用安裝 typescript,而是直接使用線上的 TypeScript Playground 來學(xué)習(xí)新的語法或新特性。
TypeScript Playground:https://www.typescriptlang.org/play/
二、TypeScript 基礎(chǔ)類型
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泛型語法
//?ES5:var?list?=?[1,2,3];
2.5 Enum 類型
使用枚舉我們可以定義一些帶名字的常量。 使用枚舉可以清晰地表達意圖或創(chuàng)建一組有區(qū)別的用例。 TypeScript 支持?jǐn)?shù)字的和基于字符串的枚舉。
1.數(shù)字枚舉
enum?Direction?{
??NORTH,
??SOUTH,
??EAST,
??WEST,
}
let?dir:?Direction?=?Direction.NORTH;
默認(rèn)情況下,NORTH 的初始值為 0,其余的成員會從 1 開始自動增長。換句話說,Direction.SOUTH 的值為 1,Direction.EAST 的值為 2,Direction.WEST 的值為 3。上面的枚舉示例代碼經(jīng)過編譯后會生成以下代碼:
"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;
當(dāng)然我們也可以設(shè)置 NORTH 的初始值,比如:
enum?Direction?{
??NORTH?=?3,
??SOUTH,
??EAST,
??WEST,
}
2.字符串枚舉
在 TypeScript 2.4 版本,允許我們使用字符串枚舉。在一個字符串枚舉里,每個成員都必須用字符串字面量,或另外一個字符串枚舉成員進行初始化。
enum?Direction?{
??NORTH?=?"NORTH",
??SOUTH?=?"SOUTH",
??EAST?=?"EAST",
??WEST?=?"WEST",
}
3.異構(gòu)枚舉
異構(gòu)枚舉的成員值是數(shù)字和字符串的混合:
enum?Enum?{
??A,
??B,
??C?=?"C",
??D?=?"D",
??E?=?8,
??F,
}
2.6 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.7 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 變量的所有賦值都被認(rèn)為是類型正確的。但是,當(dāng)我們嘗試將類型為 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)在讓我們看看當(dāng)我們嘗試對類型為 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 后,這些操作都不再被認(rèn)為是類型正確的。通過將 any 類型改變?yōu)?unknown 類型,我們已將允許所有更改的默認(rèn)設(shè)置,更改為禁止任何更改。
2.8 Tuple 類型
眾所周知,數(shù)組一般由同種類型的值組成,但有時我們需要在單個變量中存儲不同類型的值,這時候我們就可以使用元組。在 JavaScript 中是沒有元組的,元組是 TypeScript 中特有的類型,其工作方式類似于數(shù)組。
元組可用于定義具有有限數(shù)量的未命名屬性的類型。每個屬性都有一個關(guān)聯(lián)的類型。使用元組時,必須提供每個屬性的值。為了更直觀地理解元組的概念,我們來看一個具體的例子:
let?tupleType:?[string,?boolean];
tupleType?=?["Semlinker",?true];
在上面代碼中,我們定義了一個名為 tupleType 的變量,它的類型是一個類型數(shù)組 [string, boolean],然后我們按照正確的類型依次初始化 tupleType 變量。與數(shù)組一樣,我們可以通過下標(biāo)來訪問元組中的元素:
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'.
很明顯是因為類型不匹配導(dǎo)致的。在元組初始化的時候,我們還必須提供每個屬性的值,不然也會出現(xiàn)錯誤,比如:
tupleType?=?["Semlinker"];
此時,TypeScript 編譯器會提示以下錯誤信息:
Property?'1'?is?missing?in?type?'[string]'?but?required?in?type?'[string,?boolean]'.
2.9 Void 類型
某種程度上來說,void 類型像是與 any 類型相反,它表示沒有任何類型。當(dāng)一個函數(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 或 null:
let?unusable:?void?=?undefined;
2.10 Null 和 Undefined 類型
TypeScript 里,undefined 和 null 兩者有各自的類型分別為 undefined 和 null。
let?u:?undefined?=?undefined;
let?n:?null?=?null;
默認(rèn)情況下 null 和 undefined 是所有類型的子類型。 就是說你可以把 null 和 undefined 賦值給 number 類型的變量。然而,如果你指定了--strictNullChecks 標(biāo)記,null 和 undefined 只能賦值給 void 和它們各自的類型。
2.11 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;
??}
}
三、TypeScript 斷言
有時候你會遇到這樣的情況,你會比 TypeScript 更了解某個值的詳細(xì)信息。通常這會發(fā)生在你清楚地知道一個實體具有比它現(xiàn)有類型更確切的類型。
通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在干什么”。類型斷言好比其他語言里的類型轉(zhuǎn)換,但是不進行特殊的數(shù)據(jù)檢查和解構(gòu)。它沒有運行時的影響,只是在編譯階段起作用。
類型斷言有兩種形式:
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;
四、類型守衛(wèi)
A type guard is some expression that performs a runtime check that guarantees the type in some scope. —— TypeScript 官方文檔
類型保護是可執(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 值的類型。
5.2 可辨識聯(lián)合
TypeScript 可辨識聯(lián)合(Discriminated Unions)類型,也稱為代數(shù)數(shù)據(jù)類型或標(biāo)簽聯(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)有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。
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 函數(shù)
7.1 TypeScript 函數(shù)與 JavaScript 函數(shù)的區(qū)別
| TypeScript | JavaScript |
|---|---|
| 含有類型 | 無類型 |
| 箭頭函數(shù) | 箭頭函數(shù)(ES2015) |
| 函數(shù)類型 | 無函數(shù)類型 |
| 必填和可選參數(shù) | 所有參數(shù)都是可選的 |
| 默認(rèn)參數(shù) | 默認(rèn)參數(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ù)及默認(rèn)參數(shù)
//?可選參數(shù)
function?createUserId(name:?string,?age?:?number,?id:?number):?string?{
??return?name?+?id;
}
//?默認(rèn)參數(shù)
function?createUserId(
??name:?string?=?"Semlinker",
??age?:?number,
??id:?number
):?string?{
??return?name?+?id;
}
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)建多個方法的一種能力。要解決前面遇到的問題,方法就是為同一個函數(shù)提供多個函數(shù)類型定義來進行函數(shù)重載,編譯器會根據(jù)這個列表去處理函數(shù)的調(diào)用。
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;
}
在以上代碼中,我們?yōu)?add 函數(shù)提供了多個函數(shù)類型定義,從而實現(xiàn)函數(shù)的重載。之后,可惡的錯誤消息又消失了,因為這時 result 變量的類型是 string 類型。在 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");
這里需要注意的是,當(dāng) 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 類型,它與 Array 相似,只是把所有可變方法去掉了,因此可以確保數(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!
十一、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 訪問器
在 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.3 類的繼承
繼承 (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);
??}
??move(distanceInMeters?=?5)?{
????console.log("Slithering...");
????super.move(distanceInMeters);
??}
}
let?sam?=?new?Snake("Sammy?the?Python");
sam.move();
11.4 ECMAScript 私有字段
在 TypeScript 3.8 版本就開始支持ECMAScript 私有字段,使用方式如下:
class?Person?{
??#name:?string;
??constructor(name:?string)?{
????this.#name?=?name;
??}
??greet()?{
????console.log(`Hello,?my?name?is?${this.#name}!`);
??}
}
let?semlinker?=?new?Person("Semlinker");
semlinker.#name;
//?????~~~~~
//?Property?'#name'?is?not?accessible?outside?class?'Person'
//?because?it?has?a?private?identifier.
與常規(guī)屬性(甚至使用 private 修飾符聲明的屬性)不同,私有字段要牢記以下規(guī)則:
私有字段以 #字符開頭,有時我們稱之為私有名稱;每個私有字段名稱都唯一地限定于其包含的類; 不能在私有字段上使用 TypeScript 可訪問性修飾符(如 public 或 private); 私有字段不能在包含的類之外訪問,甚至不能被檢測到。
十二、TypeScript 泛型
軟件工程中,我們不僅要創(chuàng)建一致的定義良好的 API,同時也要考慮可重用性。 組件不僅能夠支持當(dāng)前的數(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)建可復(fù)用的組件要更好,因為泛型會保留參數(shù)類型。
12.1 泛型接口
interface?GenericIdentityFn?{
??(arg:?T):?T;
}
12.2 泛型類
class?GenericNumber?{
??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 這些泛型變量時,估計會一臉懵逼。其實這些大寫字母并沒有什么本質(zhì)的區(qū)別,只不過是一個約定好的規(guī)范而已。也就是說使用大寫字母 A-Z 定義的類型變量都屬于泛型,把 T 換成 A,也是一樣的。下面我們介紹一下一些常見泛型變量代表的意思:
T(Type):表示一個 TypeScript 類型 K(Key):表示對象中的鍵類型 V(Value):表示對象中的值類型 E(Element):表示元素類型
12.4 泛型工具類型
為了方便開發(fā)者 TypeScript 內(nèi)置了一些常用的工具類型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考慮,這里我們只簡單介紹 Partial 工具類型。不過在具體介紹之前,我們得先介紹一些相關(guān)的基礎(chǔ)知識,方便讀者自行學(xué)習(xí)其它的工具類型。
1.typeof
在 TypeScript 中,typeof 操作符可以用來獲取一個變量聲明或?qū)ο蟮念愋汀?/p>
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?extends?(
??...args:?any[]
)?=>?infer?R???R?:?any;
以上代碼中 infer R 就是聲明一個變量來承載傳入函數(shù)簽名的返回值類型,簡單說就是用它取到函數(shù)返回值的類型方便之后使用。
5.extends
有時候我們定義的泛型不想過于靈活或者說想繼承某些類等,可以通過 extends 關(guān)鍵字添加泛型約束。
interface?ILengthwise?{
??length:?number;
}
function?loggingIdentity<T?extends?ILengthwise>(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 的作用就是將某個類型里的屬性全部變?yōu)榭蛇x項 ?。
定義:
/**
?*?node_modules/typescript/lib/lib.es5.d.ts
?*?Make?all?properties?in?T?optional
?*/
type?Partial?=?{
??[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 )?{
??return?{?...todo,?...fieldsToUpdate?};
}
const?todo1?=?{
??title:?"organize?desk",
??description:?"clear?clutter",
};
const?todo2?=?updateTodo(todo1,?{
??description:?"throw?out?trash",
});
在上面的 updateTodo 方法中,我們利用 Partial 工具類型,定義 fieldsToUpdate 的類型為 Partial,即:
{
???title?:?string?|?undefined;
???description?:?string?|?undefined;
}
十三、TypeScript 裝飾器
13.1 裝飾器是什么
它是一個表達式 該表達式被執(zhí)行后,返回一個函數(shù) 函數(shù)的入?yún)⒎謩e為 target、name 和 descriptor 執(zhí)行該函數(shù)后,可能返回 descriptor 對象,用于配置 target 對象
13.2 裝飾器的分類
類裝飾器(Class decorators) 屬性裝飾器(Property decorators) 方法裝飾器(Method decorators) 參數(shù)裝飾器(Parameter decorators)
13.3 類裝飾器
類裝飾器聲明:
declare?type?ClassDecorator?=?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.greet();?//?console?output:?'Hello?Semlinker!';
上面的例子中,我們定義了 Greeter 類裝飾器,同時我們使用了 @Greeter 語法糖,來使用裝飾器。
友情提示:讀者可以直接復(fù)制上面的代碼,在 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.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ù),來跟蹤用戶對屬性的操作,當(dāng)代碼成功運行后,在控制臺會輸出以下結(jié)果:
Set: name => semlinker
Set: name => kakuqo
13.5 方法裝飾器
方法裝飾器聲明:
declare?type?MethodDecorator?=?(target:Object,?propertyKey:?string?|?symbol,????
??descriptor:?TypePropertyDescript)?=>?TypedPropertyDescriptor?|?void;
方法裝飾器顧名思義,用來裝飾類的方法。它接收三個參數(shù):
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);?
下面我們來介紹一下參數(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;?
??}
}
//?console?output:?The?parameter?in?position?0?
//?at?Greeter?has?been?decorated
介紹完 TypeScript 入門相關(guān)的基礎(chǔ)知識,猜測很多剛?cè)腴T的小伙伴已有 “從入門到放棄” 的想法,最后我們來簡單介紹一下編譯上下文。
十四、編譯上下文
14.1 tsconfig.json 的作用
用于標(biāo)識 TypeScript 項目的根路徑; 用于配置 TypeScript 編譯器; 用于指定編譯的文件。
14.2 tsconfig.json 重要字段
files - 設(shè)置要編譯的文件的名稱; include - 設(shè)置需要進行編譯的文件,支持路徑模式匹配; exclude - 設(shè)置無需進行編譯的文件,支持路徑模式匹配; compilerOptions - 設(shè)置與編譯流程相關(guān)的選項。
14.3 compilerOptions 選項
compilerOptions 支持很多選項,常見的有 baseUrl、 target、baseUrl、 moduleResolution 和 lib 等。
compilerOptions 每個選項的詳細(xì)說明如下:
{
??"compilerOptions":?{
????/*?基本選項?*/
????"target":?"es5",???????????????????????//?指定?ECMAScript?目標(biāo)版本:?'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?導(dǎo)入輔助工具函數(shù)
????"isolatedModules":?true,???????????????//?將每個文件做為單獨的模塊?(與?'ts.transpileModule'?類似).
????/*?嚴(yán)格的類型檢查選項?*/
????"strict":?true,????????????????????????//?啟用所有嚴(yán)格類型檢查選項
????"noImplicitAny":?true,?????????????????//?在表達式和聲明上有隱含的?any類型時報錯
????"strictNullChecks":?true,??????????????//?啟用嚴(yán)格的?null?檢查
????"noImplicitThis":?true,????????????????//?當(dāng)?this?表達式值為?any?類型的時候,生成一個錯誤
????"alwaysStrict":?true,??????????????????//?以嚴(yán)格模式檢查每個模塊,并在每個文件里加入?'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è)置默認(rèn)導(dǎo)出的模塊中默認(rèn)導(dǎo)入。
????/*?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ù)的支持
??}
}
看到這里的讀者都是“真愛”,如果你還意猶未盡,那就來看看本人整理的 Github 上 1.5K+ 的開源項目:awesome-typescript。
https://github.com/semlinker/awesome-typescript
十五、參考資源
mariusschulz - the-unknown-type-in-typescript 深入理解 TypeScript - 編譯上下文
聚焦全棧,專注分享 Angular、TypeScript、Node.js 、Spring 技術(shù)棧等全棧干貨。

回復(fù)?0?進入重學(xué)TypeScript學(xué)習(xí)群
回復(fù)?1?獲取全棧修仙之路博客地址
