TypeScript入門

来源:https://www.cnblogs.com/zhendayong/archive/2023/02/08/17102571.html
-Advertisement-
Play Games

TypeScript入門 ​ 一、什麼是TypeScript JavaScript的超集,可以編譯成JavaScript。添加了類型系統的JavaScript,可以適用於任何規模的項目。 TypeScript特性 類型系統 從 TypeScript 的名字就可以看出來,「類型」是其最核心的特性。 我 ...


TypeScript入門

一、什麼是TypeScript

JavaScript的超集,可以編譯成JavaScript。添加了類型系統的JavaScript,可以適用於任何規模的項目。

TypeScript特性

類型系統

從 TypeScript 的名字就可以看出來,「類型」是其最核心的特性。

我們知道,JavaScript 是一門非常靈活的編程語言:

  • 它沒有類型約束,一個變數可能初始化時是字元串,過一會兒又被賦值為數字。
  • 由於隱式類型轉換的存在,有的變數的類型很難在運行前就確定。
  • 基於原型的面向對象編程,使得原型上的屬性或方法可以在運行時被修改。
  • 函數是 JavaScript 中的一等公民,可以賦值給變數,也可以當作參數或返回值。

這種靈活性就像一把雙刃劍,一方面使得 JavaScript 蓬勃發展,無所不能,從 2013 年開始就一直蟬聯最普遍使用的編程語言排行榜冠軍[3];另一方面也使得它的代碼質量參差不齊,維護成本高,運行時錯誤多。

而 TypeScript 的類型系統,在很大程度上彌補了 JavaScript 的缺點。

TypeScript 是靜態類型

類型系統按照「類型檢查的時機」來分類,可以分為動態類型和靜態類型。

動態類型是指在運行時才會進行類型檢查,這種語言的類型錯誤往往會導致運行時錯誤。JavaScript 是一門解釋型語言,沒有編譯階段,所以它是動態類型,以下這段代碼在運行時才會報錯:

let foo = 1;
foo.split(' ');
// Uncaught TypeError: foo.split is not a function
// 運行時會報錯(foo.split 不是一個函數),造成線上 bug

靜態類型是指編譯階段就能確定每個變數的類型,這種語言的類型錯誤往往會導致語法錯誤。TypeScript 在運行前需要先編譯為 JavaScript,而在編譯階段就會進行類型檢查,所以 TypeScript 是靜態類型,這段 TypeScript 代碼在編譯階段就會報錯了:

let foo = 1;
foo.split(' ');
// Property 'split' does not exist on type 'number'.
// 編譯時會報錯(數字沒有 split 方法),無法通過編譯

你可能會奇怪,這段 TypeScript 代碼看上去和 JavaScript 沒有什麼區別呀。

沒錯!大部分 JavaScript 代碼都只需要經過少量的修改(或者完全不用修改)就變成 TypeScript 代碼,這得益於 TypeScript 強大的[類型推論][],即使不去手動聲明變數 foo 的類型,也能在變數初始化時自動推論出它是一個 number 類型。

完整的 TypeScript 代碼是這樣的:

let foo: number = 1;
foo.split(' ');
// Property 'split' does not exist on type 'number'.
// 編譯時會報錯(數字沒有 split 方法),無法通過編譯

TypeScript 是弱類型

類型系統按照「是否允許隱式類型轉換」來分類,可以分為強類型和弱類型。

以下這段代碼不管是在 JavaScript 中還是在 TypeScript 中都是可以正常運行的,運行時數字 1 會被隱式類型轉換為字元串 '1',加號 + 被識別為字元串拼接,所以列印出結果是字元串 '11'

console.log(1 + '1');
// 列印出字元串 '11'

TypeScript 是完全相容 JavaScript 的,它不會修改 JavaScript 運行時的特性,所以它們都是弱類型

二、安裝並編譯TypeScript

安裝TypeSscript需要nodejs環境,如果電腦沒有npm命令的,可以去官網下載並安裝nodejs

https://nodejs.org/en/

image-20220420102800410

TypeScript安裝命令

npm install -g typescript
//通過tsc --version 可以檢查版本號確保全裝成功

安裝以後編譯ts文件很簡單,我們在電腦上新建一個目錄,code,新建一個文件index.ts

然後在當前目錄下輸入命令:

tsc index.ts

編譯完成後會在當面目錄下輸出一個index.js文件,編譯成功。

我們有時候想指定目錄進行輸出:

tsc --outFile ./js/index.js index.ts

三、基本數據類型

布爾值

布爾值是最基礎的數據類型,在 TypeScript 中,使用 boolean 定義布爾值類型:

let isDone: boolean = false;

數值

使用 number 定義數值類型:

let num : number = 1;

字元串

使用 string 定義字元串類型:

let myName: string = 'Tom';
// 模板字元串
let sentence: string = `Hello, my name is ${myName}.`;

空值

JavaScript 沒有空值(Void)的概念,在 TypeScript 中,可以用 void 表示沒有任何返回值的函數:

function alertName(): void {
    alert('My name is Tom');
}

聲明一個 void 類型的變數沒有什麼用,因為你只能將它賦值為 undefinednull

let unusable: void = undefined;

Null 和 Undefined

在 TypeScript 中,可以使用 nullundefined 來定義這兩個原始數據類型:

let u: undefined = undefined;
let n: null = null;

四、任意值(Any)

任意值(Any)用來表示允許賦值為任意類型。

如果是一個普通類型,在賦值過程中改變類型是不被允許的:

let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

但如果是 any 類型,則允許被賦值為任意類型。

let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

在任意值上訪問任何屬性都是允許的:

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);

也允許調用任何方法:

let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');

所以,聲明一個變數為任意值之後,對它的任何操作,返回的內容的類型都是任意值

五、類型推論

如果沒有明確的指定類型,那麼 TypeScript 會依照類型推論(Type Inference)的規則推斷出一個類型。

以下代碼雖然沒有指定類型,但是會在編譯的時候報錯:

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

事實上,它等價於:

let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

TypeScript 會在沒有明確的指定類型的時候推測出一個類型,這就是類型推論。

如果定義的時候沒有賦值,不管之後有沒有賦值,都會被推斷成 any 類型而完全不被類型檢查

let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

六、聯合類型

聯合類型(Union Types)表示取值可以為多種類型中的一種。

舉個慄子

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

聯合類型使用 | 分隔每個類型。

這裡的 let myFavoriteNumber: string | number 的含義是,允許 myFavoriteNumber 的類型是 string 或者 number,但是不能是其他類型。

比如下麵這個慄子就會報錯:

let myFavoriteNumber: string | number;
myFavoriteNumber = true;

// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
//   Type 'boolean' is not assignable to type 'number'.

訪問聯合類型的屬性或方法

當 TypeScript 不確定一個聯合類型的變數到底是哪個類型的時候,我們只能訪問此聯合類型的所有類型里共有的屬性或方法

function getLength(something: string | number): number {
    return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

上例中,length 不是 stringnumber 的共有屬性,所以會報錯。

訪問 stringnumber 的共有屬性是沒問題的:

function getString(something: string | number): string {
    return something.toString();
}

聯合類型的變數在被賦值的時候,會根據類型推論的規則推斷出一個類型:

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 編譯時報錯

// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

上例中,第二行的 myFavoriteNumber 被推斷成了 string,訪問它的 length 屬性不會報錯。

而第四行的 myFavoriteNumber 被推斷成了 number,訪問它的 length 屬性時就報錯了。

七、介面

在 TypeScript 中,我們使用Interfaces來定義一個介面類型的對象。

什麼是介面

在面向對象語言中,介面(Interfaces)是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類去實現。

TypeScript的核心原則之一是對值所具有的結構進行類型檢查。 它有時被稱做“鴨式辨型法”或“結構性子類型化”。 在TypeScript里,介面的作用就是為這些類型命名和為你的代碼或第三方代碼定義契約。

舉個例子

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

上面的例子中,我們定義了一個介面 Person,接著定義了一個變數 tom,它的類型是 Person。這樣,我們就約束了 tom 的形狀必須和介面 Person 一致。

介面一般首字母大寫。有的編程語言中會建議介面的名稱加上 I 首碼。

定義的變數比介面少了一些屬性是不允許的:

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom'
};

// index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
//   Property 'age' is missing in type '{ name: string; }'.

多一些屬性也是不允許的:

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};

// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

可見,賦值的時候,變數的形狀必須和介面的形狀保持一致

可選屬性

有時我們希望不要完全匹配一個形狀,那麼可以用可選屬性:

interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom'
};
interface Person {
    name: string;
    age?: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

任意屬性

有時候我們希望一個介面允許有任意的屬性,可以使用如下方式:

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};

使用 [propName: string] 定義了任意屬性取 string 類型的值。

只讀屬性

有時候我們希望對象中的一些欄位只能在創建的時候被賦值,那麼可以用 readonly 定義只讀屬性:

interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    id: 89757,
    name: 'Tom',
    gender: 'male'
};

tom.id = 9527;

// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

上例中,使用 readonly 定義的屬性 id 初始化後,又被賦值了,所以報錯了。

八、數組

存放多個元素的集合

最簡單的方法是使用「類型 + 方括弧」來表示數組:

let fibonacci: number[] = [1, 1, 2, 3, 5];

數組的項中不允許出現其他的類型:

let fibonacci: number[] = [1, '1', 2, 3, 5];

// Type 'string' is not assignable to type 'number'.

數組的一些方法的參數也會根據數組在定義時約定的類型進行限制:

let fibonacci: number[] = [1, 1, 2, 3, 5];
fibonacci.push('8');

// Argument of type '"8"' is not assignable to parameter of type 'number'.

上例中,push 方法只允許傳入 number 類型的參數,但是卻傳了一個 "8" 類型的參數,所以報錯了。這裡 "8" 是一個字元串字面量類型,會在後續章節中詳細介紹。

也可以指定一個any類型的數組:

let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];

九、函數01,函數聲明、函數表達式

函數聲明

在 JavaScript 中,有兩種常見的定義函數的方式——函數聲明(Function Declaration)和函數表達式(Function Expression):

// 函數聲明(Function Declaration)
function sum(x, y) {
    return x + y;
}

// 函數表達式(Function Expression)
let mySum = function (x, y) {
    return x + y;
};

一個函數有輸入和輸出,要在 TypeScript 中對其進行約束,需要把輸入和輸出都考慮到,其中函數聲明的類型定義較簡單:

function sum(x: number, y: number): number {
    return x + y;
}

註意,輸入多餘的(或者少於要求的)參數,是不被允許的

function sum(x: number, y: number): number {
    return x + y;
}
sum(1, 2, 3);

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
function sum(x: number, y: number): number {
    return x + y;
}
sum(1);

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.

函數表達式

如果要我們現在寫一個對函數表達式(Function Expression)的定義,可能會寫成這樣:

let mySum = function (x: number, y: number): number {
    return x + y;
};

這是可以通過編譯的,不過事實上,上面的代碼只對等號右側的匿名函數進行了類型定義,而等號左邊的 mySum,是通過賦值操作進行類型推論而推斷出來的。如果需要我們手動給 mySum 添加類型,則應該是這樣:

let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};

註意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>

在 TypeScript 的類型定義中,=> 用來表示函數的定義,左邊是輸入類型,需要用括弧括起來,右邊是輸出類型。

在 ES6 中,=> 叫做箭頭函數,應用十分廣泛,可以參考ES6 中的箭頭函數

用介面定義函數的形狀

我們也可以使用介面的方式來定義一個函數需要符合的形狀:

interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

採用函數表達式|介面定義函數的方式時,對等號左側進行類型限制,可以保證以後對函數名賦值時保證參數個數、參數類型、返回值類型不變。

十、函數02,可選參數,參數預設值

可選參數

前面提到,輸入多餘的(或者少於要求的)參數,是不允許的。那麼如何定義可選的參數呢?

與介面中的可選屬性類似,我們用 ? 表示可選的參數:

function buildName(firstName: string, lastName?: string) {
  if (lastName) {
      return firstName + ' ' + lastName;
  } else {
      return firstName;
  }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

需要註意的是,可選參數必須接在必需參數後面。換句話說,可選參數後面不允許再出現必需參數了

function buildName(firstName?: string, lastName: string) {
    if (firstName) {
        return firstName + ' ' + lastName;
    } else {
        return lastName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName(undefined, 'Tom');

// index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.

參數預設值

在 ES6 中,我們允許給函數的參數添加預設值,TypeScript 會將添加了預設值的參數識別為可選參數

function buildName(firstName: string, lastName: string = 'Cat') {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

此時就不受「可選參數必須接在必需參數後面」的限制了:

function buildName(firstName: string = 'Tom', lastName: string) {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

十一、函數03,剩餘參數、重載

ES6 中,可以使用 ...rest 的方式獲取函數中的剩餘參數(rest 參數):

function push(array, ...items) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a: any[] = [];
push(a, 1, 2, 3);

事實上,items 是一個數組。所以我們可以用數組的類型來定義它:

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a = [];
push(a, 1, 2, 3);

註意,rest 參數只能是最後一個參數,關於 rest 參數,可以參考 ES6 中的 rest 參數

重載

重載允許一個函數接受不同數量或類型的參數時,作出不同的處理。方法名相同,參數列表和類型不同。

比如,我們需要實現一個函數 reverse,輸入數字 123 的時候,輸出反轉的數字 321,輸入字元串 'hello' 的時候,輸出反轉的字元串 'olleh'

利用聯合類型,我們可以這麼實現:

function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

然而這樣有一個缺點,就是不能夠精確的表達,輸入為數字的時候,輸出也應該為數字,輸入為字元串的時候,輸出也應該為字元串。

這時,我們可以使用重載定義多個 reverse 的函數類型:

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

上例中,我們重覆定義了多次函數 reverse,前幾次都是函數定義,最後一次是函數實現。在編輯器的代碼提示中,可以正確的看到前兩個提示。

註意,TypeScript 會優先從最前面的函數定義開始匹配,所以多個函數定義如果有包含關係,需要優先把精確的定義寫在前面。

十二、類型斷言01,語法,將一個聯合類型斷言為其中一個類型

類型斷言(Type Assertion)可以用來手動指定一個值的類型。

語法

值 as 類型

或者

<類型>值

在 tsx 語法(React 的 jsx 語法的 ts 版)中必須使用前者,即 值 as 類型

故建議大家在使用類型斷言時,統一使用 值 as 類型 這樣的語法。

類型斷言有多重用途

將一個聯合類型斷言為其中一個類型

之前提到過,當 TypeScript 不確定一個聯合類型的變數到底是哪個類型的時候,我們只能訪問此聯合類型的所有類型中共有的屬性或方法

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function getName(animal: Cat | Fish) {
    return animal.name;
}

而有時候,我們確實需要在還不確定類型的時候就訪問其中一個類型特有的屬性或方法,比如:

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof animal.swim === 'function') {
        return true;
    }
    return false;
}

// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
//   Property 'swim' does not exist on type 'Cat'.

上面的例子中,獲取 animal.swim 的時候會報錯。

此時可以使用類型斷言,將 animal 斷言成 Fish

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}

這樣就可以解決訪問 animal.swim 時報錯的問題了。

需要註意的是,類型斷言只能夠「欺騙」TypeScript 編譯器,無法避免運行時的錯誤,反而濫用類型斷言可能會導致運行時錯誤:

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function swim(animal: Cat | Fish) {
    (animal as Fish).swim();
}

const tom: Cat = {
    name: 'Tom',
    run() { console.log('run') }
};
swim(tom);
// Uncaught TypeError: animal.swim is not a function`

上面的例子編譯時不會報錯,但在運行時會報錯:

Uncaught TypeError: animal.swim is not a function`

原因是 (animal as Fish).swim() 這段代碼隱藏了 animal 可能為 Cat 的情況,將 animal 直接斷言為 Fish 了,而 TypeScript 編譯器信任了我們的斷言,故在調用 swim() 時沒有編譯錯誤。

可是 swim 函數接受的參數是 Cat | Fish,一旦傳入的參數是 Cat 類型的變數,由於 Cat 上沒有 swim 方法,就會導致運行時錯誤了。

總之,使用類型斷言時一定要格外小心,儘量避免斷言後調用方法或引用深層屬性,以減少不必要的運行時錯誤。

十三、類型斷言02,將一個父類斷言為更加具體的子類

當類之間有繼承關係時,類型斷言也是很常見的:

class ApiError extends Error {
    code: number = 0;
}
class HttpError extends Error {
    statusCode: number = 200;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}

上面的例子中,我們聲明瞭函數 isApiError,它用來判斷傳入的參數是不是 ApiError 類型,為了實現這樣一個函數,它的參數的類型肯定得是比較抽象的父類 Error,這樣的話這個函數就能接受 Error 或它的子類作為參數了。

但是由於父類 Error 中沒有 code 屬性,故直接獲取 error.code 會報錯,需要使用類型斷言獲取 (error as ApiError).code。

大家可能會註意到,在這個例子中有一個更合適的方式來判斷是不是 ApiError,那就是使用 instanceof:

class ApiError extends Error {
    code: number = 0;
}
class HttpError extends Error {
    statusCode: number = 200;
}

function isApiError(error: Error) {
    if (error instanceof ApiError) {
        return true;
    }
    return false;
}

上面的例子中,確實使用 instanceof 更加合適,因為 ApiError 是一個 JavaScript 的類,能夠通過 instanceof 來判斷 error 是否是它的實例。

但是有的情況下 ApiError 和 HttpError 不是一個真正的類,而只是一個 TypeScript 的介面(interface),介面是一個類型,不是一個真正的值,它在編譯結果中會被刪除,當然就無法使用 instanceof 來做運行時判斷了:

interface ApiError extends Error {
    code: number;
}
interface HttpError extends Error {
    statusCode: number;
}

function isApiError(error: Error) {
    if (error instanceof ApiError) {
        return true;
    }
    return false;
}

// index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.

此時就只能用類型斷言,通過判斷是否存在 code 屬性,來判斷傳入的參數是不是 ApiError 了:

interface ApiError extends Error {
    code: number;
}
interface HttpError extends Error {
    statusCode: number;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}

十四、類型斷言03,將任何一個類型斷言為 any

理想情況下,TypeScript 的類型系統運轉良好,每個值的類型都具體而精確。

當我們引用一個在此類型上不存在的屬性或方法時,就會報錯:

const foo: number = 1;
foo.length = 1;
// index.ts:2:5 - error TS2339: Property 'length' does not exist on type 'number'.

上面的例子中,數字類型的變數 foo 上是沒有 length 屬性的,故 TypeScript 給出了相應的錯誤提示。

這種錯誤提示顯然是非常有用的。

但有的時候,我們非常確定這段代碼不會出錯,比如下麵這個例子:

window.foo = 1;
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.

上面的例子中,我們需要將 window 上添加一個屬性 foo,但 TypeScript 編譯時會報錯,提示我們 window 上不存在 foo 屬性。

此時我們可以使用 as any 臨時將 window 斷言為 any 類型:

(window as any).foo = 1;

在 any 類型的變數上,訪問任何屬性都是允許的。

需要註意的是,將一個變數斷言為 any 可以說是解決 TypeScript 中類型問題的最後一個手段。

它極有可能掩蓋了真正的類型錯誤,所以如果不是非常確定,就不要使用 as any。

總之,一方面不能濫用 as any,另一方面也不要完全否定它的作用,我們需要在類型的嚴格性和開發的便利性之間掌握平衡(這也是 TypeScript 的設計理念之一),才能發揮出 TypeScript 最大的價值。

十五、類型斷言04,將 any 斷言為一個具體的類型

在日常的開發中,我們不可避免的需要處理 any 類型的變數,它們可能是由於第三方庫未能定義好自己的類型,也有可能是歷史遺留的或其他人編寫的爛代碼,還可能是受到 TypeScript 類型系統的限制而無法精確定義類型的場景。

遇到 any 類型的變數時,我們可以選擇無視它,任由它滋生更多的 any。

我們也可以選擇改進它,通過類型斷言及時的把 any 斷言為精確的類型,亡羊補牢,使我們的代碼向著高可維護性的目標發展。

舉例來說,歷史遺留的代碼中有個 getCacheData,它的返回值是 any:

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

那麼我們在使用它時,最好能夠將調用了它之後的返回值斷言成一個精確的類型,這樣就方便了後續的操作:

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

上面的例子中,我們調用完 getCacheData 之後,立即將它斷言為 Cat 類型。這樣的話明確了 tom 的類型,後續對 tom 的訪問時就有了代碼補全,提高了代碼的可維護性。

十六、類型斷言05,類型斷言的限制

從上面的例子中,我們可以總結出:

  • 聯合類型可以被斷言為其中一個類型
  • 父類可以被斷言為子類
  • 任何類型都可以被斷言為 any
  • any 可以被斷言為任何類型

那麼類型斷言有沒有什麼限制呢?是不是任何一個類型都可以被斷言為任何另一個類型呢?

答案是否定的——並不是任何一個類型都可以被斷言為任何另一個類型。

具體來說,若 A 相容 B,那麼 A 能夠被斷言為 BB 也能被斷言為 A

下麵我們通過一個簡化的例子,來理解類型斷言的限制:

interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

function testAnimal(animal: Animal) {
    return (animal as Cat);
}
function testCat(cat: Cat) {
    return (cat as Animal);
}

上面的慄子中是可以斷言的,我們再看看下麵的慄子:

interface Animal {
  name: string;
}
interface Cat { 
  run(): void;
}

function testAnimal(animal: Animal) {
  return (animal as Cat);
}
function testCat(cat: Cat) {
  return (cat as Animal);
}

這個時候會提示錯誤,兩者不能充分重疊,這意味要想斷言成功, 還必須具備一個條件:

  • 要使得 A 能夠被斷言為 B,只需要 A 相容 BB 相容 A 即可

十七、類型斷言06,雙重斷言

既然:

  • 任何類型都可以被斷言為 any
  • any 可以被斷言為任何類型

那麼我們是不是可以使用雙重斷言 as any as Foo 來將任何一個類型斷言為任何另一個類型呢?

interface Cat {
    run(): void;
}
interface Fish {
    swim(): void;
}

function testCat(cat: Cat) {
    return (cat as any as Fish);
}

在上面的例子中,若直接使用 cat as Fish 肯定會報錯,因為 CatFish 互相都不相容。

但是若使用雙重斷言,則可以打破「要使得 A 能夠被斷言為 B,只需要 A 相容 BB 相容 A 即可」的限制,將任何一個類型斷言為任何另一個類型。

若你使用了這種雙重斷言,那麼十有八九是非常錯誤的,它很可能會導致運行時錯誤。

除非迫不得已,千萬別用雙重斷言。

十八、類型斷言07,類型斷言 vs 類型轉換

類型斷言只會影響 TypeScript 編譯時的類型,類型斷言語句在編譯結果中會被刪除:

function toBoolean(something: any): boolean {
    return something as boolean;
}

toBoolean(1);
// 返回值為 1

在上面的例子中,將 something 斷言為 boolean 雖然可以通過編譯,但是並沒有什麼用,代碼在編譯後會變成:

function toBoolean(something) {
    return something;
}

toBoolean(1);
// 返回值為 1

所以類型斷言不是類型轉換,它不會真的影響到變數的類型。

若要進行類型轉換,需要直接調用類型轉換的方法:

function toBoolean(something: any): boolean {
    return Boolean(something);
}

toBoolean(1);
// 返回值為 true

十九、類型斷言08,類型斷言 vs 類型聲明

在這個例子中:

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

我們使用 as Catany 類型斷言為了 Cat 類型。

但實際上還有其他方式可以解決這個問題:

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom: Cat = getCacheData('tom');
tom.run();

上面的例子中,我們通過類型聲明的方式,將 tom 聲明為 Cat,然後再將 any 類型的 getCacheData('tom') 賦值給 Cat 類型的 tom

這和類型斷言是非常相似的,而且產生的結果也幾乎是一樣的——tom 在接下來的代碼中都變成了 Cat 類型。

它們的區別,可以通過這個例子來理解:

interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

const animal: Animal = {
    name: 'tom'
};
let tom = animal as Cat;

在上面的例子中,由於 Animal 相容 Cat,故可以將 animal 斷言為 Cat 賦值給 tom

但是若直接聲明 tomCat 類型:

interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

const animal: Animal = {
    name: 'tom'
};
let tom: Cat = animal;

// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.

則會報錯,不允許將 animal 賦值為 Cat 類型的 tom

我們可以得出結論:

//a斷言為b時,a和b有重疊的部分即可
//a聲明為b時,a必須具備b的所有屬性和方法

知道了它們的核心區別,就知道了類型聲明是比類型斷言更加嚴格的。

所以為了增加代碼的質量,我們最好優先使用類型聲明,這也比類型斷言的 as 語法更加優雅。

二十、類型斷言09,類型斷言 vs 泛型

還是這個例子:

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

我們還有第三種方式可以解決這個問題,那就是泛型:

function getCacheData<T>(key: string): T {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData<Cat>('tom');
tom.run();

通過給 getCacheData 函數添加了一個泛型 <T>,我們可以更加規範的實現對 getCacheData 返回值的約束,這也同時去除掉了代碼中的 any,是最優的一個解決方案。

二一、使用type關鍵字定義類型別名和字元串字面量類型

我們來看一個方法:

function getName(n: string| (() => string)): string {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

類型別名用來給一個類型起個新名字

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

上例中,我們使用 type 創建類型別名。

類型別名常用於聯合類型。

字元串字面量類型用來約束取值只能是某幾個字元串中的一個

type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
    // do something
}

handleEvent(document.getElementById('hello'), 'scroll');  // 沒問題
handleEvent(document.getElementById('world'), 'dblclick'); // 報錯,event 不能為 'dblclick'

上例中,我們使用 type 定了一個字元串字面量類型 EventNames,它只能取三種字元串中的一種。

註意,類型別名與字元串字面量類型都是使用 type 進行定義。

二二、元組

數組合併了相同類型的對象,而元組(Tuple)合併了不同類型的對象。

元組起源於函數編程語言(如 F#),這些語言中會頻繁使用元組。

舉個例子

定義一對值分別為 stringnumber 的元組:

let tom: [string, number] = ['Tom', 25];

當賦值或訪問一個已知索引的元素時,會得到正確的類型:

let tom: [string, number];
tom[0] = 'Tom';
tom[1] = 25;

也可以只賦值其中一項:

let tom: [string, number];
tom[0] = 'Tom';

但是當直接對元組類型的變數進行初始化或者賦值的時候,需要提供所有元組類型中指定的項。

let tom: [string, number];
tom = ['Tom', 25];

下麵這樣就不行:

let tom: [string, number];
tom = ['Tom'];

// Property '1' is missing in type '[string]' but required in type '[string, number]'.

越界的元素

當添加越界的元素時,它的類型會被限製為元組中每個類型的聯合類型:

let tom: [string, number];
tom = ['Tom', 25];
tom.push('male');//可以添加string
tom.push(true);//但不能添加boolean

// Argument of type 'true' is not assignable to parameter of type 'string | number'.

二三、枚舉

枚舉(Enum)類型用於取值被限定在一定範圍內的場景,比如一周只能有七天,顏色限定為紅綠藍等。

枚舉使用 enum 關鍵字來定義:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

枚舉成員會被賦值為從 0 開始遞增的數字,同時也會對枚舉值到枚舉名進行反向映射:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true

事實上,上面的例子會被編譯為:

var Days;
(function (Days) {
    Days[Days["Sun"] = 0] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days[Days["Wed"] = 3] = "Wed";
    Days[Days["Thu"] = 4] = "Thu";
    Days[Days["Fri"] = 5] = "Fri";
    Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

二四、類01,概念、構造函數、屬性和方法

類的概念

雖然 JavaScript 中有類的概念,但是可能大多數 JavaScript 程式員並不是非常熟悉類,這裡對類相關的概念做一個簡單的介紹。

  • 類(Class):定義了一件事物的抽象特點,包含它的屬性和方法
  • 對象(Object):類的實例,通過 new 生成
  • 面向對象(OOP)的三大特性:封裝、繼承、多態
  • 封裝(Encapsulation):將對數據的操作細節隱藏起來,只暴露對外的介面。外界調用端不需要(也不可能)知道細節,就能通過對外提供的介面來訪問該對象,同時也保證了外界無法任意更改對象內部的數據
  • 繼承(Inheritance):子類繼承父類,子類除了擁有父類的所有特性外,還有一些更具體的特性
  • 多態(Polymorphism):由繼承而產生了相關的不同的類,對同一個方法可以有不同的響應。比如 CatDog 都繼承自 Animal,但是分別實現了自己的 eat 方法。此時針對某一個實例,我們無需瞭解它是 Cat 還是 Dog,就可以直接調用 eat 方法,程式會自動判斷出來應該如何執行 eat
  • 存取器(getter & setter):用以改變屬性的讀取和賦值行為
  • 修飾符(Modifiers):修飾符是一些關鍵字,用於限定成員或類型的性質。比如 public 表示公有屬性或方法
  • 抽象類(Abstract Class):抽象類是供其他類繼承的基類,抽象類不允許被實例化。抽象類中的抽象方法必須在子類中被實現
  • 介面(Interfaces):不同類之間公有的屬性或方法,可以抽象成一個介面。介面可以被類實現(implements)。一個類只能繼承自另一個類,但是可以實現多個介面

使用 class 定義類,使用 constructor 定義構造函數。

通過 new 生成新實例的時候,會自動調用構造函數。

class Animal {
  public _name;
  constructor(name:string) {
      this._name = name;
  }
  sayHi() {
      return `My name is ${this._name}`;
  }
}

let a = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

二五、類02,存取器:get,set

使用 getter 和 setter 可以改變屬性的賦值和讀取行為:

class Animal {
    constructor(name) {
      this.name = name;
    }
    get name() {
      return 'Jack';
    }
    set name(value) {
      console.log('setter: ' + value);
    }
  }
  
  let a = new Animal('Kitty'); // setter: Kitty
  a.name = 'Tom'; // setter: Tom
  console.log(a.name); // Jack

二六、類03,靜態方法

使用 static 修飾符修飾的方法稱為靜態方法,它們不需要實例化,而是直接通過類來調用:

class Animal {
    public _name;
    constructor(name:string) {
        this._name = name;
    }
    sayHi() {
        return `My name is ${this._name}`;
    } 
    static sayHello(){
      return "hello,你好呀"
    }
  }
  
  let a = new Animal('Jack');
  console.log(a.sayHi()); // My name is Jack 
  console.log(Animal.sayHello()); // hello,你好呀

二七、類04,三種訪問修飾符:public、private 和 protected

  • public:全局的,公共的,當前類所涉及到的地方都可以使用

    class Animal {
      public _name;
      public constructor(name:string) {
        this._name = name;
      }
    }
    
    let a = new Animal('Jack');
    console.log(a._name); // Jack
    a._name = 'Tom';
    console.log(a._name); // Tom
    
  • private:私有的,只能在類的內部使用,無法在實例化後通過類的實例.屬性來訪問

    class Animal {
      private _name;
      public constructor(name:string) {
        this._name = name;
      }
    }
    
    let a = new Animal('Jack');
    console.log(a._name); // 報錯
    a._name = 'Tom';       // 報錯
    console.log(a._name); // 報錯
    
  • protected:受保護的,private不允許子類訪問,使用protected就可以了

    class Animal {
      protected name;
      public constructor(name:string) {
        this.name = name;
      }
    }
    
    class Cat extends Animal {
      constructor(name:string) {
        super(name);
        console.log(this.name);
      }
    }
    

二八、類05,參數屬性和只讀屬性關鍵字

修飾符和readonly還可以使用在構造函數參數中,等同於類中定義該屬性同時給該屬性賦值,使代碼更簡潔。

class Animal {
  // public name: string;
  public constructor(public name: string) {
    // this.name = name;
  }
}

只讀屬性

class Animal {
  readonly name;
  public constructor(name:string) {
    this.name = name;
  }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.

二九、類06,抽象類

abstract 用於定義抽象類和其中的抽象方法。

什麼是抽象類?

首先,抽象類是不允許被實例化的:

abstract class Animal {
  public name;
  public constructor(name:string) {
    this.name = name;
  }
  public abstract sayHi():void;
}

let a = new Animal('Jack');

// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.

上面的例子中,我們定義了一個抽象類 Animal,並且定義了一個抽象方法 sayHi。在實例化抽象類的時候報錯了。

其次,抽象類中的抽象方法必須被子類實現:

abstract class Animal {
  public name;
  public constructor(name: string) {
    this.name = name;
  }
  public abstract sayHi(): void;
}

class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`);
  } 
}

let cat = new Cat('Tom');

// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.

上面的例子中,我們定義了一個類 Cat 繼承了抽象類 Animal,但是沒有實現抽象方法 sayHi,所以編譯報錯了。

下麵是一個正確使用抽象類的例子:

abstract class Animal {
  public name;
  public constructor(name) {
    this.name = name;
  }
  public abstract sayHi();
}

class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}

let cat = new Cat('Tom');

三十、類與介面,類繼承介面,介面繼承介面,介面繼承類

類繼承介面

實現(implements)是面向對象中的一個重要概念。一般來講,一個類只能繼承自另一個類,有時候不同類之間可以有一些共有的特性,這時候就可以把特性提取成介面(interfaces),用 implements 關鍵字來實現。這個特性大大提高了面向對象的靈活性。

舉例來說,門是一個類,防盜門是門的子類。如果防盜門有一個報警器的功能,我們可以簡單的給防盜門添加一個報警方法。這時候如果有另一個類,車,也有報警器的功能,就可以考慮把報警器提取出來,作為一個介面,防盜門和車都去實現它:

interface Alarm {
  alert(): void;
}

class Door {
}

class SecurityDoor extends Door implements Alarm {
  alert() {
      console.log('SecurityDoor alert');
  }
}

class Car implements Alarm {
  alert() {
      console.log('Car alert');
  }
}

一個類可以實現多個介面:

interface Alarm {
  alert(): void;
}

interface Light {
  lightOn(): void;
  lightOff(): void;
}

class Car implements Alarm, Light {
  alert() {
    console.log('Car alert');
  }
  lightOn() {
    console.log('Car light on');
  }
  lightOff() {
    console.log('Car light off');
  }
}

上例中,Car 實現了 AlarmLight 介面,既能報警,也能開關車燈。

介面繼承介面

介面與介面之間可以是繼承關係:

interface Alarm {
    alert(): void;
}

interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}

這很好理解,LightableAlarm 繼承了 Alarm,除了擁有 alert 方法之外,還擁有兩個新方法 lightOnlightOff

介面繼承類

常見的面向對象語言中,介面是不能繼承類的,但是在 TypeScript 中卻是可以的:

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

但是這裡不推薦這樣去使用,我們在定義介面的時候只做定義,具體實現通過繼承介面的類去實現。養成好習慣。

三一、泛型01,概念,簡單示例

泛型(Generics)是指在定義函數、介面或類的時候,不預先指定具體的類型,而在使用的時候再指定類型的一種特性。

首先,我們來實現一個函數 createArray,它可以創建一個指定長度的數組,同時將每一項都填充一個預設值:

function createArray(length: number, value: any): Array<any> {
    let result = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

上例中,我們使用了之前提到過的數組泛型來定義返回值的類型。

這段代碼編譯不會報錯,但是一個顯而易見的缺陷是,它並沒有準確的定義返回值的類型:

Array 允許數組的每一項都為任意類型。但是我們預期的是,數組中每一項都應該是輸入的 value 的類型。

這時候,泛型就派上用場了:

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

上例中,我們在函數名後添加了 <T>,其中 T 用來指代任意輸入的類型,在後面的輸入 value: T 和輸出 Array<T> 中即可使用了。

接著在調用的時候,可以指定它具體的類型為 string。當然,也可以不手動指定,而讓類型推論自動推算出來:

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

三二、泛型02,多個類型參數

定義泛型的時候,可以一次定義多個類型參數:

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

上例中,我們定義了一個 swap 函數,用來交換輸入的元組。

三三、泛型03,泛型約束

在函數內部使用泛型變數的時候,由於事先不知道它是哪種類型,所以不能隨意的操作它的屬性或方法:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);
    return arg;
}

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含屬性 length,所以編譯的時候報錯了。

這時,我們可以對泛型進行約束,只允許這個函數傳入那些包含 length 屬性的變數。這就是泛型約束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

上例中,我們使用了 extends 約束了泛型 T 必須符合介面 Lengthwise 的形狀,也就是必須包含 length 屬性。

此時如果調用 loggingIdentity 的時候,傳入的 arg 不包含 length,那麼在編譯階段就會報錯了:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

loggingIdentity(7);

// index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.

三四、泛型04,泛型介面

之前學習過,可以使用介面的方式來定義一個函數需要符合的形狀:

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

當然也可以使用含有泛型的介面來定義函數的形狀:

interface CreateArrayFunc {
    <T>(length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

進一步,我們可以把泛型參數提前到介面名上:

interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

註意,此時在使用泛型介面的時候,需要定義泛型的類型。

三五、泛型05,泛型類

與泛型介面類似,泛型也可以用於類的類型定義中:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

泛型參數的預設類型

在 TypeScript 2.3 以後,我們可以為泛型中的類型參數指定預設類型。當使用泛型時沒有在代碼中直接指定類型參數,從實際值參數中也無法推測出時,這個預設類型就會起作用。

function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

三六、聲明合併,同名函數、介面、類的合併

如果定義了兩個相同名字的函數、介面或類,那麼它們會合併成一個類型:

函數的合併

之前學習過,我們可以使用重載定義多個函數類型:

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

介面的合併

介面中的屬性在合併時會簡單的合併到一個介面中:

interface Alarm {
    price: number;
}
interface Alarm {
    weight: number;
}

相當於:

interface Alarm {
    price: number;
    weight: number;
}

註意,合併的屬性的類型必須是唯一的

interface Alarm {
    price: number;
}
interface Alarm {
    price: number;  // 雖然重覆了,但是類型都是 `number`,所以不會報錯
    weight: number;
}
interface Alarm {
    price: number;
}
interface Alarm {
    price: string;  // 類型不一致,會報錯
    weight: number;
}

// index.ts(5,3): error TS2403: Subsequent variable declarations must have the same type.  Variable 'price' must be of type 'number', but here has type 'string'.

介面中方法的合併,與函數的合併一樣:

interface Alarm {
    price: number;
    alert(s: string): string;
}
interface Alarm {
    weight: number;
    alert(s: string, n: number): string;
}

相當於:

interface Alarm {
    price: number;
    weight: number;
    alert(s: string): string;
    alert(s: string, n: number): string;
}

類的合併

類的合併與介面的合併規則一致。

PS:但是一般情況下,不建議創建多個同名介面或者類,雖然可以自動合併,但是可能會發現意想不到的問題。代碼不要寫在兩個地方,不然不好維護。

三七、寫在結尾

TypeScript的應用非常廣泛,最新的Vue和React均集成了TypeScript,這裡推薦大家使用Vue3,Vue3天然支持TS。

另外一方面,TypeScript中有很多的ES語法,這裡用一張圖來示意兩者之間的關係:

所以對ES5或者ES6不瞭解的同學,可以學習ES,瞭解一下相關的語法,這對於學習TypeScript也有一定的幫助。

在學習TS的過程中,也推薦大家多看官方文檔,官網的文檔比較詳細和豐富:

https://www.tslang.cn/docs/home.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大家都知道 MySQL 的數據都是保存在磁碟的,那具體是保存在哪個文件呢?MySQL 存儲的行為是由存儲引擎實現的,MySQL 支持多種存儲引擎,不同的存儲引擎保存的文件自然也不同。InnoDB 是我們常用的存儲引擎,也是 MySQL 預設的存儲引擎。本文主要以 InnoDB 存儲引擎展開討論。 ...
  • NineData 是多雲數據管理平臺(https://www.ninedata.cloud/),致力於讓每個人用好數據和雲。作為資料庫領域的技術創新團隊,面對這麼火ChatGPT,我們 NineData 的工程師也針對ChatGPT,做了一些關於資料庫領域的相關測試,測試結果,真的是智商狂飆。 ...
  • vuex是什麼? vuex是管理應用程式狀態,實現組件間通信的。 為什麼使用vuex? 在開發大型應用的項目時,會出現多個視圖組件依賴一個同一個狀態,來自不同視圖的行為需要變更同一個狀態。 在遇到以上問題,就要用到vuex,他能把組件的共用狀態抽取出來,當做一個全局單例模式進行管理,不管在何處改變狀 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 很多前端都喜歡用 console.log 調試,先不談調試效率怎麼樣,首先 console.log 有個致命的問題:會導致記憶體泄漏。 為什麼這麼說呢? 用 Performance 和 Memory 工具分析下就知道了。 我們準備這樣一段代 ...
  • 1.【配置】應用版本號名稱有一個規則的字元串:1.0.0,規則是:大版本號,中版本號,小版本號。 2.【配置】應用版本號中的小版本號不能超過9,超過9的需要向上一個版本號進一(逢十進一)。 3.【配置】應用版本號是一個整數類型,最長10位,超過10位就會被自動轉化成字元串。 4.【配置】應用版本號和 ...
  • 函數: 一個被設計為執行特定任務的代碼塊 語法 通過function 關鍵詞定義,後面跟著其函數名稱,然後是一對圓括弧,圓括弧中可以定義一些函數的參數。沒有名稱的函數呢? 函數名稱可以包含字母、數字、下劃線、中劃線和美元符號(命名規則與變數命名一致)。 // 聲明一個函數 function fnNa ...
  • 回顧第一篇文章中談到的組件庫的幾個方面,只剩下最後的、也是最重要的組件庫的打包構建、本地發佈、遠程發佈了。 1 組件庫構建 組件庫的入口是 packages/yyg-demo-ui,構建組件庫有兩個步驟: 添加 TypeScript 的配置文件: tsconfig.json 添加 vite.conf ...
  • 圖片資源,在我們的業務中可謂是占據了非常大頭的一環,尤其是其對帶寬的消耗是十分巨大的。 對圖片的性能優化及體驗優化在今天就顯得尤為重要。本文,就將從各個方面闡述,在各種新特性滿頭飛的今天,我們可以如何儘可能的對我們的圖片資源,進行性能優化及體驗優化。 圖片類型的選取及 Picture 標簽的使用 首 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...