背景介紹
JavaScript創(chuàng)立20多年,已經(jīng)從當(dāng)初只是為網(wǎng)頁(yè)添加瑣碎交互的小型腳本語(yǔ)言發(fā)展成應(yīng)用最廣泛的跨平臺(tái)語(yǔ)言之一。

雖然用JavaScript可編寫(xiě)的程序無(wú)論從規(guī)模、范圍、復(fù)雜性都呈指數(shù)級(jí)增長(zhǎng),但因?yàn)镴avaScript弱類(lèi)型特性使得其在項(xiàng)目管理層面表現(xiàn)的相當(dāng)差勁。科技巨頭微軟為了解決這樣的尷尬問(wèn)題推出了Typescript。

內(nèi)容梗概
之所以命名為T(mén)ypescript文檔是因?yàn)楸疚氖菍?duì)日常開(kāi)發(fā)中的ts語(yǔ)法全鏈路指引。
看完收獲
1、熟悉常用的ts語(yǔ)法
2、清楚常用的編譯選項(xiàng)
3、正確預(yù)判各種場(chǎng)景下的ts特性
Typescript特性
Static type-checking(靜態(tài)類(lèi)型檢測(cè))
先看一個(gè)js的例子
const message = "message";
message.toFixed(2);
根據(jù)以上js代碼段,如果在瀏覽器中執(zhí)行,你會(huì)得到如下錯(cuò)誤:

我們?cè)賹⑸弦欢畏湃雝s文件中

在代碼運(yùn)行之前,ts就會(huì)給我們一條錯(cuò)誤消息,這樣的特性就叫static type-checking(靜態(tài)類(lèi)型檢測(cè))。
Non-exception Failures(非異常的失?。?/h4>
還是以一個(gè)js實(shí)例引入話(huà)題:
const user = {
name: "Daniel",
age: 26,
};
user.location; // returns undefined
如我們熟知的那樣,在js中取一個(gè)對(duì)象不存在的屬性,那么它會(huì)返回undefined。
我們將代碼移入ts文件:

ts會(huì)通過(guò)靜態(tài)檢測(cè)機(jī)制報(bào)錯(cuò)。像這種在js中不屬于異常范疇的ts報(bào)錯(cuò),就稱(chēng)之Non-exception Failures(非異常的失敗)。
此類(lèi)失?。o態(tài)類(lèi)型檢測(cè)不通過(guò)),我們?cè)倭谐鰩讉€(gè)
未正確的調(diào)用的方法:

邏輯錯(cuò)誤:

Types for Tooling(語(yǔ)法提示)
前面我們講了ts的異常攔截還有非異常攔截,足以讓我們感受到ts的錯(cuò)誤攔截的強(qiáng)悍之處,但是ts還有個(gè)重磅功能:前置的語(yǔ)法提示!
看圖:

當(dāng)我們?cè)谥С謙s語(yǔ)法提示的編輯器(本文使用vscode演示)鍵入字符串變量之后的"."操作,會(huì)自動(dòng)提示出來(lái)很多的相關(guān)操作。
tsc, TypeScript 編譯器
我們一直在談?wù)擃?lèi)型檢查,但我們還沒(méi)有使用我們的類(lèi)型檢查器。 下面隆重推出tsc,TypeScript 編譯器。
我們通過(guò)npm install -g typescript來(lái)安裝。
tsc hello.ts
tsc hello.ts可以將hello.ts編譯成js文件
Erased Types(擦除的類(lèi)型)
什么是擦除的類(lèi)型,當(dāng)我們使用tsc對(duì)hello.ts文件進(jìn)行編譯時(shí),我們能得到如下結(jié)果
轉(zhuǎn)換前:

轉(zhuǎn)換后:

Downleveling(語(yǔ)法降級(jí))
細(xì)心的你會(huì)發(fā)現(xiàn),tsc指令除了會(huì)將代碼中的類(lèi)型擦除之外,還對(duì)代碼進(jìn)行了語(yǔ)法降級(jí),模版字符的變成了字符拼接,是因?yàn)閠sc默認(rèn)采用ECMAScript 3或者ECMAScript 5標(biāo)準(zhǔn)來(lái)編譯。
如果我們想要指定編譯規(guī)范,可以這么寫(xiě):tsc --target es2015 hello.ts
ts的基礎(chǔ)類(lèi)型:string,number, boolean
ts中使用最頻繁的類(lèi)型就是字符、數(shù)字和布爾值。有一點(diǎn)要注意,這幾個(gè)類(lèi)型表述都是小寫(xiě),千萬(wàn)不要跟 js當(dāng)中的String, Number, Boolean混淆。
Arrays(數(shù)組類(lèi)型)
常見(jiàn)的數(shù)組類(lèi)型表達(dá)T[]和Array<T>比如:
[1,2,3,4]一個(gè)數(shù)字集合的數(shù)組類(lèi)型,我們可以用number[]及Array<number>這樣的語(yǔ)法來(lái)表示。
記住一點(diǎn)[number] 可不是數(shù)組類(lèi)型,它是一個(gè)標(biāo)準(zhǔn)的 Tuples(元組)。元組具體是啥,后續(xù)有詳細(xì)介紹。
any(任意類(lèi)型)
如其名,如果設(shè)置了某個(gè)變量的類(lèi)型為any類(lèi)型,那么不單單是這個(gè)變量本身類(lèi)型不會(huì)被檢測(cè),它的屬性和方法也會(huì)繞過(guò)ts的類(lèi)型檢測(cè),都成立。
Type Annotations on Variables(在變量聲明的同時(shí)指定其類(lèi)型)
先看怎么定義:
let myName: string = "Alice";
定義方式就是”變量:類(lèi)型“這樣的方式
但是ts有個(gè)特性:類(lèi)型推斷。如上例子咱們也可以直接:
let myName = "Alice";
ts也會(huì)根據(jù)賦值類(lèi)型,推斷出來(lái)變量的類(lèi)型。
關(guān)于標(biāo)量是否需要指定其類(lèi)型,官方說(shuō):在定義變量的時(shí)候,盡可能的少指定其類(lèi)型:
For the most part you don’t need to explicitly learn the rules of inference. If you’re starting out, try using fewer type annotations than you think - you might be surprised how few you need for TypeScript to fully understand what’s going on.
Functions(函數(shù))
一個(gè)例子:
function greet(name: string): string {
return 'Hello, ' + name.toUpperCase() + '!!'
}
函數(shù)的參數(shù)類(lèi)型和返回類(lèi)型如上例所示。后續(xù)有詳盡的函數(shù)篇幅介紹。
Object Types(對(duì)象類(lèi)型)
一個(gè)例子:
function printCoord(pt: { x: number; y: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
類(lèi)型表述 { x: number; y: number },將所有的對(duì)象屬性一一列出就是對(duì)象類(lèi)型的基礎(chǔ)用法。
參數(shù)可選:
function printCoord(pt: { x: number; y?: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
咱們先快速過(guò)基礎(chǔ)用法,后續(xù)會(huì)有詳細(xì)的對(duì)象類(lèi)型語(yǔ)法介紹。
Union Types(聯(lián)合類(lèi)型)
一個(gè)例子:
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
語(yǔ)法表達(dá):number | string,聯(lián)合類(lèi)型的意思恰如其名,用來(lái)表達(dá)一個(gè)變量包含一種以上的類(lèi)型表達(dá)。
聯(lián)合類(lèi)型帶來(lái)的麻煩:

如何解決:
function printId(id: number | string) {
if (typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}
我們通過(guò)一定的條件過(guò)濾將類(lèi)型做了劃分,最終解決了類(lèi)型交叉的問(wèn)題。
Type Aliases( 類(lèi)型別名語(yǔ)法:Type)
前面我說(shuō)過(guò)聯(lián)合類(lèi)型的用法,那么是不是每次用到聯(lián)合類(lèi)型都要字面量的方式“type1 | type2”去定義呢?
當(dāng)然不是,Type關(guān)鍵的字申明類(lèi)型別名可以幫你解決:
type ID = number | string;
Interfaces( 類(lèi)型語(yǔ)法:Interfaces)
一個(gè)例子:
interface Point {
x: number;
y: number;
}
Differences Between Type Aliases and Interfaces(Type和Interface的區(qū)別)

以上是官方的說(shuō)法,作為一個(gè)TS重度支持者,我用一句大白話(huà)說(shuō)下,我對(duì)與type和interface的用法區(qū)別,type用作支撐類(lèi)型“計(jì)算”,比如我們?cè)谠?lèi)型基礎(chǔ)之上重新定義一個(gè)新類(lèi)型。
看個(gè)例子:
interface Person {
name: string
age?: number
}
type NewPerson = Person & {
age: number
}
Type Assertions(類(lèi)型斷言)
何為斷言,示例中找的第一個(gè)例子是獲取dom元素的例子,正如我們清楚的那樣,獲取 dom要么是獲取到null要么是獲取到具體的dom元素。那么如果我們后續(xù)想要使用這個(gè)dom元素的操作,就需要使用具體的元素類(lèi)型,比如:HTMLCanvasElement。對(duì)于這樣的場(chǎng)景,我們可以在語(yǔ)法中加入斷言(我非常確定我獲取到的dom元素非null且就是我認(rèn)為的那種類(lèi)型?。?。斷言是 TS中的肯定語(yǔ)氣。
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
const a = (expr as any) as T; //有時(shí)候需要繞一下
要點(diǎn)1 :as是最常用的斷言方式,但是如果看到<Type>variable這樣的語(yǔ)法咱們也得知道這也是斷言。
要點(diǎn)2:咱們實(shí)際開(kāi)發(fā)過(guò)程中會(huì)碰到有些類(lèi)型沒(méi)辦法直接斷言的情況,那么我們就需要斷言?xún)纱?,通常?huì)使用as any或者as unknown作為第一層斷言“(expr as any) as T”
Literal Types( 字面類(lèi)型)
先看一個(gè)基礎(chǔ)例子:
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
Type '"howdy"' is not assignable to type '"hello"'.
再來(lái)一個(gè)聯(lián)合類(lèi)型的例子:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
再來(lái)一個(gè)聯(lián)合類(lèi)型的例子:
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
Non-null Assertion Operator (Postfix!)(ts的非undefined和null的斷言修飾符“!”)
TypeScript還有一種特殊語(yǔ)法,用于在不進(jìn)行任何顯式檢查的情況下從類(lèi)型中刪除null和undefined。具體實(shí)例如下:
function liveDangerously(x?: number | null) {
console.log(x.toFixed());
//Object is possibly 'null' or 'undefined'
console.log(x!.toFixed());
}
ts中所有的具體值都能作為類(lèi)型使用,這樣的語(yǔ)法就叫做字面類(lèi)型
Enums(枚舉)
枚舉是TypeScript為數(shù)不多的特性之一,它不是JavaScript的類(lèi)型級(jí)擴(kuò)展。
Numeric enums(數(shù)值枚舉)
enum Direction {
Up = 1,
Down,
Left,
Right,
}
enum Direction {
Up,
Down,
Left,
Right,
}
數(shù)值枚舉,默認(rèn)的起始數(shù)值是1,后續(xù)以此遞增。如果指定了頭部值,那么默認(rèn)值就會(huì)變成指定值。
String enums(字符枚舉)
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
枚舉常用在字面化表達(dá)一個(gè)變量的不同類(lèi)型值,比如:性別。
Less Common Primitives(不常用的類(lèi)型)
bigint
從ES2020開(kāi)始,JavaScript中有一個(gè)用于非常大整數(shù)的原語(yǔ)BigInt:
// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);
// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;
symbol
JavaScript中有一個(gè)原語(yǔ),用于通過(guò)函數(shù)Symbol()創(chuàng)建全局唯一引用:
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.
// Can't ever happen
}
Narrowing(類(lèi)型過(guò)篩)

這張圖是我找了半天用來(lái)表示TS語(yǔ)法的Narrowing的圖,我給語(yǔ)法想了一個(gè)中文特性的詞:過(guò)篩(網(wǎng)絡(luò)上都習(xí)慣說(shuō)窄化,但是我覺(jué)得太生硬)。正如圖片所示,Narrowing其實(shí)就是表述TS類(lèi)型歸類(lèi)的語(yǔ)法集。
typeof type guards(typeof攔截)
JavaScript 支持 typeof 運(yùn)算符,它可以返回變量的類(lèi)型字符。 TypeScript也同樣,看結(jié)果列表:
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
利用typeof縮小類(lèi)型范圍
function printAll(str: string | number): void {
if (typeof str === 'string') {
console.log(str)
} else {
console.log(str + '')
}
}
Truthiness narrowing(真實(shí)性)
先看個(gè)例子:
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
上例攔截了數(shù)字0,如果沒(méi)有在線(xiàn)人數(shù)則顯示"無(wú)人在此",在js中某些值做if判斷,會(huì)是false。
這些值如下:
0
NaN
"" (the empty string)
0n (the bigint version of zero)
null
undefined
Equality narrowing(等于判斷)
來(lái)個(gè)例子:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// We can now call any 'string' method on 'x' or 'y'.
x.toUpperCase();
y.toLowerCase();
} else {
console.log(x);
console.log(y);
}
}
如上代碼,如果想要x全等于y,那么只能是string類(lèi)型。
再來(lái)個(gè)例子:
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// Remove both 'null' and 'undefined' from the type.
if (container.value != null) { //這是重點(diǎn)要考
console.log(container.value);
(property) Container.value: number
// Now we can safely multiply 'container.value'.
container.value *= factor;
}
}
Thein operator narrowing(in操作符)
看個(gè)例子:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
再看個(gè)例子:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal;
// (parameter) animal: Fish | Human
} else {
animal;
// (parameter) animal: Bird | Human
}
}
當(dāng)一個(gè)屬性為可選字段的時(shí)候,用in判斷,它會(huì)出現(xiàn)在true和false的兩邊。
instanceof narrowing(instance操作符)
一個(gè)例子:
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
(parameter) x: Date
} else {
console.log(x.toUpperCase());
(parameter) x: string
}
}
Assignments(賦值)
一個(gè)例子:
let x = Math.random() < 0.5 ? 10 : "hello world!";
x = 1;
console.log(x);
let x: number
x = true;
Type 'boolean' is not assignable to type 'string | number'.
console.log(x);
Using type predicates(使用類(lèi)型謂詞)
看個(gè)例子:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
The never type(never類(lèi)型)
縮小范圍時(shí),你可以將類(lèi)型縮小到一無(wú)所有的程度。 在這些情況下,TypeScript 將使用 never 類(lèi)型來(lái)表示不應(yīng)該存在的狀態(tài)。
Exhaustiveness checking(詳盡檢測(cè))
never 類(lèi)型可分配給每種類(lèi)型; 但是,沒(méi)有任何類(lèi)型可以分配給 never(除了 never 本身)。 這意味著你可以使用縮小范圍并依靠從不出現(xiàn)在 switch 語(yǔ)句中進(jìn)行詳盡的檢查。
看個(gè)例子:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
再看個(gè)例子:
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
Type 'Triangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}
正如前面敘述的內(nèi)容,never不能類(lèi)型不能賦值成其他類(lèi)型之外的任意類(lèi)型。
More on Functions(關(guān)于function的更多內(nèi)容)
函數(shù)是任何應(yīng)用程序的基本構(gòu)建塊,無(wú)論它們是本地函數(shù)、從另一個(gè)模塊導(dǎo)入的函數(shù)還是類(lèi)上的方法。它們也是值,就像其他值一樣,TypeScript有很多方法來(lái)描述如何調(diào)用函數(shù)。讓我們學(xué)習(xí)如何編寫(xiě)描述函數(shù)的類(lèi)型。
Function Type Expressions(函數(shù)表達(dá)式)
最簡(jiǎn)單的函數(shù)類(lèi)型表達(dá)就是箭頭函數(shù)
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
轉(zhuǎn)變一下:
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
Call Signatures(調(diào)用簽名)
前面我們說(shuō)了箭頭函數(shù)的函數(shù)類(lèi)型表達(dá),接下來(lái)我們看下另外一種:
type DescribableFunction = {
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(" returned " + fn(6));
}
在JavaScript中,函數(shù)除了可以調(diào)用外,還可以具有屬性。但是,函數(shù)類(lèi)型表達(dá)式語(yǔ)法不允許聲明屬性。如果我們想用屬性描述可調(diào)用的內(nèi)容,可以在對(duì)象類(lèi)型中編寫(xiě)調(diào)用簽名:
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
Construct Signatures(構(gòu)造簽名)
還可以使用新操作符調(diào)用JavaScript函數(shù)。TypeScript將它們稱(chēng)為構(gòu)造函數(shù),因?yàn)樗鼈兺ǔ?chuàng)建一個(gè)新對(duì)象。您可以通過(guò)在調(diào)用簽名前添加新關(guān)鍵字來(lái)編寫(xiě)構(gòu)造簽名:
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
有些對(duì)象,如JavaScript的Date對(duì)象,可以使用new調(diào)用,也可以不使用new調(diào)用。您可以任意組合同一類(lèi)型的調(diào)用和構(gòu)造簽名:
interface CallOrConstruct {
new (s: string): Date;
(n?: number): number;
}
Generic Functions(范型函數(shù))
通常編寫(xiě)一個(gè)函數(shù),其中輸入的類(lèi)型與輸出的類(lèi)型相關(guān),或者兩個(gè)輸入的類(lèi)型以某種方式相關(guān)。讓我們考慮一下返回?cái)?shù)組第一個(gè)元素的函數(shù):
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);
Inference(主動(dòng)推斷)
注意,我們不必在這個(gè)示例中指定類(lèi)型。類(lèi)型由TypeScript推斷(自動(dòng)選擇)。
我們也可以使用多個(gè)類(lèi)型參數(shù)。例如,map的獨(dú)立版本如下所示:
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
以上兩個(gè)例子,解釋了范型函數(shù)提供了類(lèi)型入口,讓我們方便做類(lèi)型管理。咱們?cè)倏匆粋€(gè)最通用的范型例子:
interface ApiBaseRes<T> {
code: number
msg: string
data: T
}
Constraints(范型的類(lèi)型約束)
范型的類(lèi)型也是可以有約束的,我們來(lái)看一個(gè)例子:
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
Working with Constrained Values(返回值約束)
前面我們說(shuō)了范型可以約束參數(shù),那么同樣的也能約束返回值,一個(gè)例子:
function minimumLength<Type extends { length: number }>(
obj: Type,
): Type {
return obj
}
再來(lái)看一個(gè)插常見(jiàn)的例子:
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
Type '{ length: number; }' is not assignable to type 'Type'.
'{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
}
}
Specifying Type Arguments(指定類(lèi)型參數(shù))
例子說(shuō)話(huà):
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
const arr = combine([1, 2, 3], ["hello"]);
Type 'string' is not assignable to type 'number'.
修正方案:
const arr = combine<string | number>([1, 2, 3], ["hello"]);
Push Type Parameters Down(推翻參數(shù)約束)
看個(gè)例子:
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);
記住一個(gè)規(guī)則:如果可能,請(qǐng)使用類(lèi)型參數(shù)本身,而不是對(duì)其進(jìn)行約束
Use Fewer Type Parameters(少使用范型的參數(shù))
看兩個(gè)表達(dá)式:
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
規(guī)則:始終盡可能少的使用范型參數(shù)
Type Parameters Should Appear Twice(使用范型參數(shù)需要參數(shù)出現(xiàn)兩次以上)
//錯(cuò)誤的例子
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
greet("world");
//正確的例子
function greet(s: string) {
console.log("Hello, " + s);
}
規(guī)則:如果類(lèi)型參數(shù)只出現(xiàn)在一個(gè)位置,請(qǐng)重新考慮是否確實(shí)需要它(想象一下一個(gè)一個(gè)只使用一次的變量)
Optional Parameters(可選參數(shù))
兩個(gè)例子說(shuō)明:
//case 1
function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK
//case 2
function f(x = 10) {
// ...
}
Function Overloads(方法重載)
一些JavaScript函數(shù)可以在各種參數(shù)計(jì)數(shù)和類(lèi)型中調(diào)用。例如,您可以編寫(xiě)一個(gè)函數(shù)來(lái)生成一個(gè)日期,該日期采用時(shí)間戳(一個(gè)參數(shù))或月/日/年規(guī)范(三個(gè)參數(shù))。
在TypeScript中,我們可以指定一個(gè)函數(shù),該函數(shù)可以通過(guò)編寫(xiě)重載簽名以不同的方式調(diào)用。為此,請(qǐng)編寫(xiě)一些函數(shù)簽名(通常是兩個(gè)或更多),然后是函數(shù)體:
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);
No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
Overload Signatures and the Implementation Signature(簽名和實(shí)現(xiàn)簽名)
看個(gè)例子:
function fn(x: string): void;
function fn() {
// ...
}
// Expected to be able to call with zero arguments
fn();
為什么錯(cuò)呢?因?yàn)閠s以申明簽名為準(zhǔn)。
另外實(shí)現(xiàn)簽名還必須與重載簽名兼容。例如,這些函數(shù)有錯(cuò)誤,因?yàn)閷?shí)現(xiàn)簽名與重載不匹配:
先看個(gè)例子:
function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void;
This overload signature is not compatible with its implementation signature.
function fn(x: boolean) {}
修復(fù)方案:
function fn(x: boolean): void;
function fn(x: string): void;
function fn(x: boolean|string) {}
Writing Good Overloads(編寫(xiě)良好的重載)
看個(gè)例子:
//兩種尷尬
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) { //問(wèn)題1:如果不寫(xiě)any那么就要寫(xiě)string | any[]
return x.length;
}
len(""); // OK
len([0]); // OK
//問(wèn)題2:并不像我們想象的那樣,調(diào)用也有問(wèn)題
len(Math.random() > 0.5 ? "hello" : [0]);
No overload matches this call.
Overload 1 of 2, '(s: string): number', gave the following error.
Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'.
Type 'number[]' is not assignable to type 'string'.
Overload 2 of 2, '(arr: any[]): number', gave the following error.
Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'.
Type 'string' is not assignable to type 'any[]'.
來(lái)個(gè)最樸素的烹飪方式:
function len(x: any[] | string) {
return x.length;
}
規(guī)則:如果可能,請(qǐng)始終首選具有聯(lián)合類(lèi)型的參數(shù),而不是重載參數(shù)。
Other Types to Know About(跟函數(shù)相關(guān)的其他類(lèi)型)
void表示無(wú)返回
function noop():void {
}
記住void跟undefined不一樣
object
在ts中object類(lèi)型表示除了string、number、bingint、boolean、symbol、null、undefined類(lèi)型之外其他類(lèi)型。有人會(huì)問(wèn)那么object是void嗎?void只表示函數(shù)不返回。
unknown
unknown可以代表任意類(lèi)型,但是不同于any,它不能像any那樣擁有任意屬性和方法。

never
never跟void類(lèi)似,只用在函數(shù)的返回表達(dá),看兩個(gè)例子:
function fail(msg: string): never {
throw new Error(msg);
}
function fn(x: string | number) {
if (typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x; // has type 'never'!
}
}
Function
一個(gè)例子:
function doSomething(f: Function) {
return f(1, 2, 3);
}
Rest Parameters and Arguments(剩余的函數(shù)參數(shù)和傳參)
Rest Parameters(剩余的函數(shù)參數(shù))
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);
Rest Arguments(剩余的傳參)
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
const args = [8, 5];
const angle = Math.atan2(...args);
Parameter Destructuring(參數(shù)解構(gòu))
一個(gè)例子:
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
console.log(a + b + c);
}
Object Types(對(duì)象類(lèi)型)
一個(gè)最常見(jiàn)的對(duì)象類(lèi)型表達(dá):
interface Person {
name: string;
age: number;
}
Property Modifiers(屬性特性)
Optional Properties(可選屬性)
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos;
(property) PaintOptions.xPos?: number | undefined
let yPos = opts.yPos;
(property) PaintOptions.yPos?: number | undefined
// ...
}
如何給可選屬性添加默認(rèn)值:
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos);
(parameter) xPos: number
console.log("y coordinate at", yPos);
(parameter) yPos: number
// ...
}
readonly Properties(只讀屬性)
一個(gè)例子:
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
// But we can't re-assign it.
obj.prop = "hello";
Cannot assign to 'prop' because it is a read-only property.
}
有人問(wèn),我能不能改變屬性變成非只讀,答案是能。在mapping modifiers里有解答
Index Signatures(索引簽名?)
先看個(gè)例子:
interface StringArray {
[index: number]: string;
}
Extending Types(對(duì)象繼承)
一個(gè)例子:
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
Intersection Types (交叉類(lèi)型)
一個(gè)例子:
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// okay
draw({ color: "blue", radius: 42 });
// oops
draw({ color: "red", raidus: 42 });
Argument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'.
Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?
Generic Object Types(對(duì)象類(lèi)型的范型)
一個(gè)例子:
interface Box<Type> {
contents: Type;
}
interface StringBox {
contents: string;
}
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
(property) Box<string>.contents: string
let boxB: StringBox = { contents: "world" };
boxB.contents;
(property) StringBox.contents: string
The Array Type(數(shù)組類(lèi)型)
看個(gè)例子:
function doSomething(value: Array<string>) {
// ...
}
let myArray: string[] = ["hello", "world"];
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
數(shù)組表達(dá)Type[]或者Array<Type>
The ReadonlyArray Type(只讀的數(shù)組類(lèi)型)
一個(gè)例子:
function doStuff(values: ReadonlyArray<string>) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
Property 'push' does not exist on type 'readonly string[]'.
}
Tuple Types(元組類(lèi)型)
type StringNumberPair = [string, number];
function doSomething(pair: [string, number]) {
const a = pair[0];
const a: string
const b = pair[1];
const b: number
// ...
}
doSomething(["hello", 42]);
function doSomething(pair: [string, number]) {
// ...
const c = pair[2];
Tuple type '[string, number]' of length '2' has no element at index '2'.
}
元組不僅是約束了元素類(lèi)型,甚至還約束了元素個(gè)數(shù)。
readonly Tuple Types(只讀的元組類(lèi)型)
看個(gè)例子:
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
Cannot assign to '0' because it is a read-only property.
}
Type Manipulation (類(lèi)型操作)
Creating Types from Types(以類(lèi)型創(chuàng)建新類(lèi)型)
Generics(范型)
范型的啟動(dòng)式,我想實(shí)現(xiàn)一個(gè)識(shí)別函數(shù)。
步驟一,實(shí)現(xiàn)識(shí)別固定類(lèi)型:
function identity(arg: number): number {
return arg;
}
步驟二,放開(kāi)類(lèi)型限制
function identity(arg: any): any {
return arg;
}
步驟三、牽出范型
function identity<Type>(arg: Type): Type {
return arg;
}
驗(yàn)證調(diào)用:
let output = identity<string>("myString");
第二種調(diào)用
let output = identity("myString");
ts會(huì)根據(jù)傳入的參數(shù)類(lèi)型,反推返回類(lèi)型
Working with Generic Type Variables(使用范型的變量)
function identity<Type>(arg: Type): Type {
return arg;
}
Generic Classes(范型類(lèi))
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
Generic Constraints(范型約束)
范型作為一個(gè)預(yù)置類(lèi)型參數(shù),有沒(méi)有一種語(yǔ)法可以約束其類(lèi)型呢?答案是有的。
先看個(gè)例子:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
Property 'length' does not exist on type 'Type'.
return arg;
}
怎么破:
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
Using Type Parameters in Generic Constraints(利用范型約束入?yún)ⅲ?/h6>
可以聲明受其他類(lèi)型參數(shù)約束的類(lèi)型參數(shù)。例如,這里我們想從給定名稱(chēng)的對(duì)象中獲取屬性。我們希望確保不會(huì)意外獲取obj上不存在的屬性,因此我們將在這兩種類(lèi)型之間放置一個(gè)約束:
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
Using Class Types in Generics(在范型中使用calss)
一個(gè)例子:
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
Keyof Type Operator(keyof類(lèi)型操作符)
一個(gè)例子:
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
//type A = number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
//type M = string | number
請(qǐng)注意,在本例中,M的類(lèi)型為string或者number-這是因?yàn)镴avaScript對(duì)象鍵總是強(qiáng)制為字符串,所以obj[0]總是與obj[“0”]相同。
typeof and ReturnType type operator(typeof和ReturnType類(lèi)型操作符)
先看typeof:

再看ReturnType:

typeof跟js的略微有所不同,它是一種返回類(lèi)型的高級(jí)語(yǔ)法,如上圖,它是可以返回類(lèi)似這樣的類(lèi)型表達(dá)的:
() => { x:number, y:number }
Indexed Access Types(索引值方案類(lèi)型)
一張圖案例:

Conditional Types(條件類(lèi)型)
基礎(chǔ)的條件類(lèi)型實(shí)例:
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
type Example1 = number
type Example2 = RegExp extends Animal ? number : string;
type Example2 = string
//表達(dá)式
//SomeType extends OtherType ? TrueType : FalseType;
再來(lái)一個(gè)實(shí)例說(shuō)明為什么要有條件類(lèi)型這種語(yǔ)法:
//重載
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}
//condition type
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript");
let a: NameLabel
let b = createLabel(2.8);
let b: IdLabel
let c = createLabel(Math.random() ? "hello" : 42);
let c: NameLabel | IdLabel
將原本需要分開(kāi)表達(dá)的參數(shù)和返回值做了一定關(guān)聯(lián)約束
Conditional Type Constraints(條件類(lèi)型的約束)
一個(gè)例子:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
type EmailMessageContents = string
type DogMessageContents = MessageOf<Dog>;
type DogMessageContents = never
再看一個(gè)例子:
type Flatten<T> = T extends any[] ? T[number] : T;
// Extracts out the element type.
type Str = Flatten<string[]>;
type Str = string
// Leaves the type alone.
type Num = Flatten<number>;
type Num = number
Inferring Within Conditional Types(條件類(lèi)型的推斷【infer】語(yǔ)法)
先來(lái)個(gè)帥氣的開(kāi)場(chǎng):
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
infer語(yǔ)法有點(diǎn)像一個(gè)別名申明,給類(lèi)型定義一個(gè)別名,后續(xù)就能繼續(xù)使用。
再來(lái)看一個(gè)函數(shù)返回使用infer語(yǔ)法:
type GetReturnType<Type> = Type extends (...args: any[]) => infer Return
? Return
: any;
type Num = GetReturnType<() => number>;
// type Num = number
type Str = GetReturnType<(x: string) => string>;
// type Str = string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
Distributive Conditional Types(條件類(lèi)型的分配行為)
一個(gè)例子:
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
type StrArrOrNumArr = string[] | number[]
當(dāng)我們使用范型和條件類(lèi)型相結(jié)合,返回類(lèi)型就變得可分配了。
Mapped Types(映射類(lèi)型)
先看個(gè)例子:
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
再來(lái)個(gè)變化的例子:
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<FeatureFlags>;
Mapping Modifiers(映射變換)
改變只讀為非只讀:
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
type UnlockedAccount = CreateMutable<LockedAccount>;
改變可選屬性變成必填:
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type User = Concrete<MaybeUser>;
Key Remapping via as(通過(guò)as語(yǔ)法改變映射字段)
看一個(gè)公式:
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
再來(lái)看一個(gè)實(shí)際的例子:
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
我們來(lái)移除一個(gè)屬性得到一個(gè)新類(lèi)型:
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
再看一個(gè)有趣的例子:
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
Template Literal Types(字面量的字符類(lèi)型)
先看個(gè)例子:
type World = "world";
type Greeting = `hello ${World}`;
type Greeting = "hello world"
再看個(gè)組合類(lèi)型的例子:
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
再來(lái)看個(gè)高級(jí)的例子:
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", newName => {
(parameter) newName: string
console.log(`new name is ${newName.toUpperCase()}`);
});
person.on("ageChanged", newAge => {
(parameter) newAge: number
if (newAge < 0) {
console.warn("warning! negative age");
}
})
再來(lái)快速過(guò)幾個(gè)類(lèi)型變換:
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
type ShoutyGreeting = "HELLO, WORLD"
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
type QuietGreeting = "hello, world"
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
Classes(類(lèi))
TS包含所有ES2015的class內(nèi)容,并且在其基礎(chǔ)上增加了類(lèi)型申明,用來(lái)加強(qiáng)不同class和類(lèi)型的關(guān)聯(lián)描述。
Class Members(類(lèi)的成員)
先看一個(gè)沒(méi)有任何屬性字段的最基礎(chǔ)的class:
class Point {}
Fileds(字段)
class的公有(public)可寫(xiě)(writeable)屬性:
class Point {
x: number;
y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
如果我們想要給字段進(jìn)行初始值設(shè)定:
class Point {
x = 0;
y = 0;
}
const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`);
class的類(lèi)型推斷:
const pt = new Point();
pt.x = "0";
//Type 'string' is not assignable to type 'number'.
Point類(lèi)的x屬性是數(shù)字類(lèi)型,上例中我們?cè)O(shè)定其值為字符串的0,就被TS類(lèi)型預(yù)警攔截了。
--strictPropertyInitialization(嚴(yán)格的屬性初始化約束)
strictPropertyInitialization設(shè)置在TS編譯選項(xiàng)中控制class字段必須要在構(gòu)造方法(constructor)中完成初始化。 當(dāng)我們?cè)O(shè)置了這樣的選項(xiàng)之后,請(qǐng)看如下兩個(gè)示例。
錯(cuò)誤的示范:
class BadGreeter {
name: string;
}
//Property 'name' has no initializer and is not definitely assigned in the constructor.
正確的示例:
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
當(dāng)然,如果我們有種方式,可以繞過(guò)TS的初始化檢查,示例如下:
class OKGreeter {
// Not initialized, but no error
name!: string;
}
readonly(只讀)
class的只讀(readonly)是在定義屬性時(shí)加前綴實(shí)現(xiàn)的,它會(huì)阻止屬性在構(gòu)造函數(shù)在外被改變。
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
this.name = "not ok";
//Cannot assign to 'name' because it is a read-only property.
}
}
const g = new Greeter();
g.name = "also not ok";
//Cannot assign to 'name' because it is a read-only property.
Constructors(構(gòu)造函數(shù))
構(gòu)造函數(shù)和普通函數(shù)基本一致,支持參數(shù)和重載
參數(shù)的例子:
class Point {
x: number;
y: number;
// Normal signature with defaults
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
重載的例子:
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
構(gòu)造函數(shù)跟普通函數(shù)之前的區(qū)別大致有兩點(diǎn):
1、構(gòu)造函數(shù)整體不能被類(lèi)型描述,構(gòu)造函數(shù)的類(lèi)型只能在class的申明表達(dá)式中描述。
2、構(gòu)造函數(shù)不能有返回類(lèi)型。
Super Calls(super語(yǔ)法調(diào)用)
如果我們?cè)谧宇?lèi)中想要使用父類(lèi)的this.property系列屬性和方法,我們需要在子類(lèi)的構(gòu)造函數(shù)中調(diào)用super,如果你不記得這個(gè)操作也不用怕,TS會(huì)提示你的。
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// Prints a wrong value in ES5; throws exception in ES6
console.log(this.k);
//'super' must be called before accessing 'this' in the constructor of a derived class.
super();
}
}
Methods(方法)
class中的function為了跟普通函數(shù)做區(qū)分,我們稱(chēng)之為方法,方法跟普通函數(shù)類(lèi)似,一個(gè)例子:
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
只有一個(gè)注意點(diǎn),在方法中使用class屬性,必須用this.語(yǔ)法,否則會(huì)觸發(fā)一些bug:
let x: number = 0;
class C {
x: string = "hello";
m() {
// This is trying to modify 'x' from line 1, not the class property
x = "world";
}
}
//Type 'string' is not assignable to type 'number'.
Getters / Setters
class擁有訪(fǎng)問(wèn)器:
class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}
三個(gè)注意點(diǎn):
1、如果只有g(shù)et那么這個(gè)屬性就是只讀
2、get屬性的類(lèi)型默認(rèn)根據(jù)return推斷得出
3、get和set成對(duì)屬性,必須要時(shí)同一種成員特性描述(只讀、可寫(xiě)等)
Index Signatures(索引簽名)
class的屬性也支持索引簽名,直接看例子:
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
Class Heritage(class的繼承)
像其他面向?qū)ο笳Z(yǔ)言一樣,TS也可以繼承基類(lèi)
implements Clauses(implements語(yǔ)法)
你可以使用implements檢查class是否按照interface的定義:
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
Class 'Ball' incorrectly implements interface 'Pingable'.
pong() {
console.log("pong!");
}
//Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
}
class也可以實(shí)現(xiàn)多個(gè)interce:
class C implements A, B {}
extends Clauses(繼承語(yǔ)法)
class可以繼承自一個(gè)基類(lèi)(base class),這個(gè)class可以包含基類(lèi)的一切屬性和方法,也可以在此基礎(chǔ)上追加一些屬性和方法??磦€(gè)例子:
class Animal {
move() {
console.log("Moving along!");
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
const d = new Dog();
// Base class method
d.move();
// Derived class method
d.woof(3);
Overriding Methods(重寫(xiě)方法)
在子類(lèi)中可以使用super關(guān)鍵詞表示父類(lèi)的實(shí)例。先看一個(gè)重寫(xiě)方法的例子:
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");
上例中我們?cè)谧宇?lèi)中改寫(xiě)了父類(lèi)的greet方法,設(shè)定了一個(gè)可選參數(shù),如果我們將這個(gè)參數(shù)改成必填,那么就不符合父類(lèi)的約束會(huì)報(bào)錯(cuò):
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
// Make this parameter required
greet(name: string) {
console.log(`Hello, ${name.toUpperCase()}`);
}
//Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'.
//Type '(name: string) => void' is not assignable to type '() => void'.
}
Type-only Field Declarations(只有類(lèi)型的字段申明)
當(dāng)我們TS編譯標(biāo)準(zhǔn)設(shè)定值大于等于ES2022版本或者我們開(kāi)啟useDefineForClassFields時(shí),class的字段會(huì)在基礎(chǔ)類(lèi)構(gòu)造函數(shù)執(zhí)行完成之后初始化。當(dāng)我們想要重寫(xiě)很多基礎(chǔ)類(lèi)的字段且只想要重新申明一個(gè)更準(zhǔn)確的繼承值時(shí)會(huì)出現(xiàn)一些問(wèn)題。為了解決這樣的問(wèn)題,我們可以重新定義字段申明。
看例子:
interface Animal {
dateOfBirth: any;
}
interface Dog extends Animal {
breed: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
// Does not emit JavaScript code,
// only ensures the types are correct
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
Initialization Order(初始化順序)
前面我們講了很多class繼承及class初始化的相關(guān)知識(shí),那么大家應(yīng)該也會(huì)好奇初始化相關(guān)的執(zhí)行順序到底是怎樣的呢?
看個(gè)例子:
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
class Derived extends Base {
name = "derived";
}
// Prints "base", not "derived"
const d = new Derived();
代碼執(zhí)行過(guò)程順序如下:
1、基礎(chǔ)類(lèi)字段初始化
2、基礎(chǔ)類(lèi)構(gòu)造函數(shù)執(zhí)行
3、子類(lèi)字段初始化
4、子類(lèi)構(gòu)造函數(shù)執(zhí)行
Member Visibility(class屬性及方法可見(jiàn)性)
你可以使用TS控制屬性在方法在class外部的可見(jiàn)性。
public(公開(kāi))
看個(gè)例子:
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
其實(shí)我們也可以省略public關(guān)鍵字,TS默認(rèn)就是public。公開(kāi)是權(quán)限完全放開(kāi)的方式。
protected(受保護(hù))
受保護(hù)的限制表示只能在子類(lèi)中使用父類(lèi)的屬性和方法,看例子:
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
// OK to access protected member here
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
//Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
Exposure of protected members(將受保護(hù)的字段暴露出去)
我們可以在子類(lèi)中改寫(xiě)父類(lèi)的區(qū)域限制:
class Base {
protected m = 10;
}
class Derived extends Base {
// No modifier, so default is 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK
private(私有)
私有化的屬性不能被自身的實(shí)例化對(duì)象訪(fǎng)問(wèn),也不能被子類(lèi)繼承。
不能在實(shí)例中訪(fǎng)問(wèn)的示例:
class Base {
private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
//Property 'x' is private and only accessible within class 'Base'.
不能在子類(lèi)中被訪(fǎng)問(wèn)的示例1:
class Derived extends Base {
showX() {
// Can't access in subclasses
console.log(this.x);
//Property 'x' is private and only accessible within class 'Base'.
}
}
不能在子類(lèi)中訪(fǎng)問(wèn)的示例2:
class Base {
private x = 0;
}
class Derived extends Base {
Class 'Derived' incorrectly extends base class 'Base'.
Property 'x' is private in type 'Base' but not in type 'Derived'.
x = 1;
}
Static Members(靜態(tài)成員)
class擁有不需要在實(shí)例化之后才能訪(fǎng)問(wèn)的屬性和方法,這些成員就叫做靜態(tài)成員。他們可以直接通過(guò)類(lèi)名訪(fǎng)問(wèn)。
一個(gè)簡(jiǎn)單的例子:
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
靜態(tài)類(lèi)型也能用可見(jiàn)約束限制:
class MyClass {
private static x = 0;
}
console.log(MyClass.x);
//Property 'x' is private and only accessible within class 'MyClass'.
靜態(tài)成員也能被繼承:
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
Special Static Names(特殊的靜態(tài)名)
因?yàn)轭?lèi)本質(zhì)是構(gòu)造函數(shù),所以函數(shù)的自帶屬性不能作為類(lèi)的靜態(tài)名,例如:name, length, and call。
看個(gè)例子:
class S {
static name = "S!";
}
//Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
Why No Static Classes?(為什么不建議用static)
試想一下,咱們平時(shí)的函數(shù)調(diào)用和對(duì)象的方法調(diào)用是不是跟class的靜態(tài)調(diào)用類(lèi)似?
演示代碼:
// Unnecessary "static" class
class MyStaticClass {
static doSomething() {}
}
// Preferred (alternative 1) 替代方案1
function doSomething() {}
// Preferred (alternative 2)替代方案2
const MyHelperObject = {
dosomething() {},
};
Generic Classes(范型class)
直接看簡(jiǎn)短的代碼演示:
class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
const b = new Box("hello!");
//const b: Box<string>
this at Runtime in Classes(class在執(zhí)行過(guò)程中的this指向)
ts的執(zhí)行的行為跟js一致,比如this指向,我們先看一個(gè)列子:
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
// Prints "obj", not "MyClass"
console.log(obj.getName());
如上例所示,getName返回的不是實(shí)例c的mame(MyClasss),而是返回的obj的name(obj),長(zhǎng)話(huà)短說(shuō),在js中有個(gè)非常有趣的this指向特性,它會(huì)根據(jù)調(diào)用環(huán)境來(lái)判斷到底this指向誰(shuí)。例子中obj調(diào)用的getName,其完整的調(diào)用棧是obj->c.getName,其調(diào)用環(huán)境是obj,obj的name是“obj”,所以return this.name就是返回的obj字符。
然而對(duì)于我們一些新手js開(kāi)發(fā)人員,我其實(shí)就想要實(shí)現(xiàn)返回實(shí)例c的name,那么我們可以怎么處理呢?
看解法1:
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
console.log(obj.getName());
箭頭函數(shù)巧妙的幫我們改變了this指向。
那么還有什么思路可以幫我們規(guī)避非常規(guī)的調(diào)用呢?
看解法2:
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
// Error, would crash
const g = c.getName;
console.log(g());
//The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
這時(shí)候提示我們的調(diào)用context(上下文)this不符合getName中this設(shè)定的MyClass類(lèi)型,也可以幫我們規(guī)避非常規(guī)的調(diào)用。
this Types(this的類(lèi)型)
在class中,最特殊的類(lèi)型就屬this了,它會(huì)動(dòng)態(tài)的推斷成當(dāng)前的class類(lèi)型,請(qǐng)看如下示例:
class Box {
contents: string = "";
set(value: string) {
this.contents = value;
return this;
}
}
上述代碼段,我們返回set的this。如果我們直接實(shí)例化Box,那么毋庸置疑返回的this就是Box類(lèi)型。

這時(shí)候我寫(xiě)個(gè)繼承類(lèi)ClearableBox:
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
const a = new ClearableBox();
const c = a.set("hello");
那么這時(shí)候的set返回的this類(lèi)型就成了ClearableBox

官方的handbook關(guān)于類(lèi)的翻譯寫(xiě)的我是真累,到此整個(gè)handbook的精編版已經(jīng)完成,后續(xù)我會(huì)找一篇TS的Class的文章繼續(xù)作為內(nèi)容補(bǔ)充,下篇預(yù)告:演繹篇