基礎(chǔ)篇—ts文檔精編版

背景介紹

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


Javascript

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


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ò)誤:


image.png

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


image.png

在代碼運(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文件:

image.png

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)用的方法:
image.png
邏輯錯(cuò)誤:
image.png

Types for Tooling(語(yǔ)法提示)

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

image.png

當(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)換前:


image.png

轉(zhuǎn)換后:


image.png

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)的麻煩:


image.png

如何解決:

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ū)別)

image.png

以上是官方的說(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ò)篩)

image.png

這張圖是我找了半天用來(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那樣擁有任意屬性和方法。


image.png
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:


image.png

再看ReturnType:


image.png

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

Indexed Access Types(索引值方案類(lèi)型)

一張圖案例:


image.png

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)型。


image.png

這時(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


image.png

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容