【TS 演化史 -- 破曉】一步一個腳印帶你入門 TS

前端小智發表於2019-11-04

作者:Valentino Gagliardi

譯者:前端小智

來源:valentinog


什麼是 TypeScript

官方網站的定義是:TypeScript 是 JS 型別的超集。它假設我們們知道什麼是超集,什麼是型別化。為了簡單起見,你可以將 TypeScript 看作是 JavaScript 之上的一個外殼

TypeScript 是一個外殼,因為編寫 TypeScript 的程式碼,在編譯之後,,剩下的只是簡單的 JS 程式碼。

但是 JS 引擎無法讀取 TypeScript 程式碼,因此任何 TypeScript 檔案都應該經過預翻譯過程,即編譯。只有在第一個編譯步驟之後,才剩下純 JS 程式碼,可以在瀏覽器中執行。稍後會介紹 TypeScript 編譯是如何完成的。

現在讓我們記住 TypeScript 是一種特殊的 JS,在瀏覽器中執行之前它需要一個翻譯。

為什麼要使用 TypeScript

剛開始,我們們不完全理解 TypeScript 為何有意義。 你可能會問“ TypeScript 的目的是什麼”。 這是一個很好的問題。

實際上,一旦它在您的程式碼中發現嚴重和愚蠢的錯誤,你就會看到 TypeScript 的好處。更重要的是,TypeScript 會讓程式碼變得結構良好而且還是自動,這些還只是 TypeScript 的一小部分。

不管怎樣,也經常有人說 TypeScript 沒用,太過笨拙。

凡事都有兩面性,TypeScript 有很多反對者和支持者,但重要的是 TypeScript 是一個可靠的工具,將它放在我們們的工具技能包中不會造成傷害。

TypeScript 配置

為什麼配置? TypeScript 還有一個二進位制檔案,可將 TypeScript 程式碼編譯為 JS 程式碼. 請記住,瀏覽器不理解 TypeScript:

mkdir typescript-tutorial && cd $_
npm init -y
複製程式碼

然後安裝 TypeScript

npm i typescript --save-dev
複製程式碼

接下來在 package.json 中的 scripts 下新增如下內容,以便我們們可以輕鬆地執行 TypeScript 編譯器:

  "scripts": {
    "tsc": "tsc"
  }
複製程式碼

tsc 代表 TypeScript 編譯器,只要編譯器執行,它將在專案資料夾中查詢名為tsconfig.json 的檔案。 使用以下命令為 TypeScript 生成配置檔案:

npm run tsc -- --init
複製程式碼

執行成功後會在控制檯收到 message TS6071: Successfully created a tsconfig.json file。在專案資料夾中會看到新增了一個 tsconfig.json 檔案。tsconfig。json 是一個可怕的配置檔案,不要慌。我們們不需要知道它的每一個要點,在下一節中,會介紹入門的相關部分。

配置TypeScript 編譯器

最好先初始化 git repo 並提交原始的 tsconfig.json,然後再開啟檔案。 我們將只保留一些配置選項,並刪除其他所有內容。 稍後,你可能需要將現在的版本與原始版本進行比較。

首先,請開啟 tsconfig.json 並將所有原始內容替換為以下內容:

{
  "compilerOptions": {
    "target": "es5",
    "strict": true
  }
}
複製程式碼

儲存並關閉檔案。 首先,你可能想知道 tsconfig.json 是幹什麼的。 該配置檔案由 TypeScript 編譯器和任何具有 TypeScript 支援的程式碼編輯器讀取。

  • noImplicitAny true:當變數沒有定義型別時,TypeScript 會報錯

  • alwaysStrict true:嚴格模式是 JS 的安全機制,它可以防止意外的全域性變數,預設的 this 繫結等。 設定為 “alwaysStrict” 時,TypeScript 在每個KS 檔案的頂部都使用 “use strict”

有更多的配置選項可用。隨著時間的推移,你會學到更多,因為現在上面的兩個選擇是你開始學習時需要知道的一切。

關於型別的幾個詞

TypeScript 支援與 JS 幾乎相同的資料型別,此外,TypeScript 自己新增了更多的型別,如 any 型別一樣。

“any” 是鬆散的 TypeScript 型別。 這意味著:此變數可以是任何型別:字串,布林值,物件等。 實際上,這就像根本沒有型別檢查。

TypeScript 中的行為

我們們從一個合法的 KS函式開始:filterByTerm。在專案資料夾中建立一個名為 filterByTerm.js 的新檔案,並輸入以下內容

function filterByTerm(input, searchTerm) {
  if (!searchTerm) throw Error("searchTerm 不能為空");
  if (!input.length) throw Error("input 不能為空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

filterByTerm("input string", "java");
複製程式碼

如果現在不瞭解邏輯,請不要擔心。 看一下該函式的引數,以及幾行之後如何使用它們。 只需檢視程式碼,就應該已經發現了問題。

我想知道是否有一種方法可以在我的 IDE 中檢查這個函式,而不需要執行程式碼或者用Jest測試它。這可能嗎? TypeScript 在這方面做得很好,實際上它是 JS 中靜態檢查的最佳工具,也就是說,在程式碼執行之前測試程式碼的正確性

因此,我們們改用 TypeScript ,將檔案的副檔名從 filterByTerm.js 改為 filterByTerm.ts。通過這種更改,你會發現程式碼中的一堆錯誤

【TS 演化史 -- 破曉】一步一個腳印帶你入門 TS

可以看到函式引數下的有很多紅色標記。從現在開始,會向你展示文字形式的錯誤,但是請記住,當我們們在TypeScript 中出錯時,IDE 和文字編輯器都會顯示這些紅線。

確定哪個地方錯:

npm run tsc
複製程式碼

可以看到控制的報錯:

filterByTerm.ts:1:23 - error TS7006: Parameter 'input' implicitly has an 'any' type.

1 function filterByTerm(input, searchTerm) {
                        ~~~~~

filterByTerm.ts:1:30 - error TS7006: Parameter 'searchTerm' implicitly has an 'any' type.

1 function filterByTerm(input, searchTerm) {
                               ~~~~~~~~~~

filterByTerm.ts:5:32 - error TS7006: Parameter 'arrayElement' implicitly has an 'any' type.

5   return input.filter(function(arrayElement) {
複製程式碼

TypeScript 告訴你函式引數具有 “any” 型別,如果還記得的話,它可以是 TypeScript 中的 any 型別。 我們需要在我們的 TypeScript 程式碼中新增適當的型別註釋。

什麼是型別,JS 中有什麼問題

到目前為止,JS 有七種型別

  • String
  • Number
  • Boolean
  • Null
  • Undefined
  • Object
  • Symbol (ES6)

除了 Object 型別外,其它是 JS 的基本資料型別。每種 JS 型別都有相應的表示,可以程式碼中使用,比如字串和數字

var name = "Hello John";
var age = 33;
複製程式碼

JS 的問題是,變數可以隨時更改其型別。例如,布林值可以變成字串(將以下程式碼儲存到名為 types.js 的檔案中)

var aBoolean = false;
console.log(typeof aBoolean); // "boolean"

aBoolean = "Tom";
console.log(typeof aBoolean); // "string"
複製程式碼

轉換可以是有意的,開發人員可能真的希望將 Tom 分配到 aBoolean,但是這類錯誤很可能是偶然發生的。

從技術上講,JS 本身沒有什麼問題,因為它的型別動態是故意的。JS 是作為一種簡單的 web 指令碼語言而誕生的,而不是作為一種成熟的企業語言。

然而,JS 鬆散的特性可能會在你的程式碼中造成嚴重的問題,破壞其可維護性。TypeScript 旨在通過向 JS 新增強型別來解決這些問題。實際上,如果將 types.js 的擴充套件更改為 types.ts 。你會在IDE中看到 TypeScript 的抱怨。

types.ts 的編譯控制檯會報錯:

types.ts:4:1 - error TS2322: Type '"Tom"' is not assignable to type 'boolean'.
複製程式碼

有了這些知識,接著,我們們更深入地研究 TypeScript 型別。

深入 TypeScript 型別

TypeScript 強調有型別,我們們上面的程式碼根本沒有型別,是時候新增一些了。首先要修正函式引數。通過觀察這個函式是如何呼叫的,它似乎以兩個字串作為引數:

filterByTerm("input string", "java");
複製程式碼

為引數新增型別:

function filterByTerm(input: string, searchTerm: string) {
    // ...
}

// ...
複製程式碼

接著編譯:

npm run tsc
複製程式碼

剩下的錯誤:

filterByTerm.ts:5:16 - error TS2339: Property 'filter' does not exist on type 'string'.
複製程式碼

可以看到 TypeScript 是如何指導我們,現在的問題在於 filter 方法。

function filterByTerm(input: string, searchTerm: string) {
  // 省略一些
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}
複製程式碼

我們們告訴 TypeScript “input” 是一個字串,但是在後面的程式碼中呼叫了 filter 方法,它屬於陣列。我們真正需要的是將輸入標記為某個東西的陣列,可能是字串陣列:

為此,有兩個選擇。選項1:string[]

function filterByTerm(input: string[], searchTerm: string) {
    // ...
}
複製程式碼

選項2: Array<Type>

function filterByTerm(input: Array<string>, searchTerm: string) {
    // ...

}
複製程式碼

我個人更喜歡選項2。 現在,嘗試再次編譯(npm run tsc),控制檯資訊如下:

filterByTerm.ts:10:14 - error TS2345: Argument of type '"input string"' is not assignable to parameter of type 'string[]'.

filterByTerm("input string", "java");
複製程式碼

TypeScript 還會校驗傳入的型別。 我們將 input 改為字串陣列:

filterByTerm(["string1", "string2", "string3"], "java");
複製程式碼

這是到目前為止的完整程式碼:

function filterByTerm(input: Array<string>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm 不能為空");
  if (!input.length) throw Error("input 不能為空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

filterByTerm(["string1", "string2", "string3"], "java");
複製程式碼

看上去很好,但是,編譯(npm run tsc)還是過不了:

filterByTerm.ts:6:25 - error TS2339: Property 'url' does not exist on type 'string'.
複製程式碼

TypeScript 確實很嚴謹。 我們傳入了一個字串陣列,但是在程式碼後面,嘗試訪問一個名為 “url” 的屬性:

return arrayElement.url.match(regex);
複製程式碼

這意味著我們們需要一個物件陣列,而不是字串陣列。我們們在下一節中解決這個問題。

TypeScript 物件和介面

上面遺留一個問題:因為 filterByTerm 被傳遞了一個字串陣列。url 屬性在型別為 string 的TypeScript 上不存在。所以我們們改用傳遞一個物件陣列來解決這個問題:

filterByTerm(
  [{ url: "string1" }, { url: "string2" }, { url: "string3" }],
  "java"
);
複製程式碼

函式定義也要對應的更改:

function filterByTerm(input: Array<object>, searchTerm: string) {
    // omitted
}
複製程式碼

現在讓我們編譯程式碼

npm run tsc
複製程式碼

控制輸出:

filterByTerm.ts:6:25 - error TS2339: Property 'url' does not exist on type 'object'.
複製程式碼

又來了,通用 JS 物件沒有任何名為 url 的屬性。對我來說,TypeScript 對型別要求真的是很嚴謹。

這裡的問題是,我們們不能給一個隨機物件分配屬性,TypeScript 的核心原則之一是對值所具有的結構進行型別檢查, 在 TypeScript 裡,介面(interface)的作用就是為這些型別命名和為你的程式碼或第三方程式碼定義契約,我們們可以使用介面來解決這個問題。

通過檢視我們的程式碼,我們可以想到一個名為 Link 的簡單**"模型"**,其結構應該符合以下模式:它必須有一個型別為 stringurl 屬性。

在TypeScript 中,你可以用一個介面來定義這個模型,就像這樣(把下面的程式碼放在 filterByTerm.ts 的頂部):

interface ILink {
  url: string;
}
複製程式碼

對於介面宣告,這當然不是有效的 JS 語法,在編譯過程中會被刪除。

提示:在定義介面名字前面加上大寫的I,這是 TypeScript 的慣例。

現在,使用使用介面 ILink 定義 input 型別

function filterByTerm(input: Array<ILink>, searchTerm: string) {
    // ...
}
複製程式碼

通過此修復,可以說 TypeScript “期望 ILink 陣列”作為該函式的輸入,以下是完整的程式碼:

interface ILink {
  url: string;
}

function filterByTerm(input: Array<ILink>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm 不能為空");
  if (!input.length) throw Error("input 不能為空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

filterByTerm(
  [{ url: "string1" }, { url: "string2" }, { url: "string3" }],
  "java"
);
複製程式碼

此時,所有的錯誤都應該消失了,可以執行了:

npm run tsc
複製程式碼

編譯後,會在專案資料夾中生成一個名為 filterByTerm.js 的檔案,其中包含純 JS 程式碼。可以檢出該檔案並檢視 TypeScript 特定的宣告最終轉換成 JS 的是什麼樣的。

因為 alwaysStrict 設定為 true,所以 TypeScript 編譯器也會在 filterByTerm.js 的頂部使用 use strict

介面和欄位

TypeScript 介面是該語言最強大的結構之一。介面有助於在整個應用程式中形成模型,這樣任何開發人員在編寫程式碼時都可以選擇這種模型並遵循它。

前面,我們們定義了一個簡單的介面 ILink

interface ILink {
  url: string;
}
複製程式碼

如果您想要向介面新增更多的欄位,只需在塊中宣告它們即可:

interface ILink {
  description: string;
  id: number;
  url: string;
}
複製程式碼

現在,型別為ILink的物件都必須實現新欄位,否則就會出現錯誤,如果把上面 的定義重新寫入 filterByTerm.ts 然後重新編譯就會報錯了:

filterByTerm.ts:17:4 - error TS2739: Type '{ url: string; }' is missing the following properties from type 'ILink': description, id
複製程式碼

問題在於我們函式的引數:

filterByTerm(
  [{ url: "string1" }, { url: "string2" }, { url: "string3" }],
  "java"
);
複製程式碼

TypeScript 可以通過函式宣告來推斷引數是 ILink 的型別陣列。因此,該陣列中的任何物件都必須實現介面 ILink 中定義的所有欄位

大多數情況下,實現所有欄位是不太現實的。畢竟,我們也不知道 ILink 型別的每個新物件是否會需要擁有所有欄位。不過不要擔心,要使編譯通過,可以宣告介面的欄位可選,使用 ? 表示:

interface ILink {
  description?: string;
  id?: number;
  url: string;
}
複製程式碼

現在編輯器和編譯器都沒問題了。然而 TypeScript 介面可以做的更多,在下一節我們將看到如何擴充套件它們。但首先簡要介紹一下 TypeScript 中的變數。

變數宣告

到目前為止,我們們已經瞭解瞭如何向函式引數中新增型別:

function filterByTerm(input: Array<ILink>, searchTerm: string) {
    //
}
複製程式碼

TypeScript 並不限於此,當然也可以向任何變數新增型別。為了說明這個例子,我們們一一地提取函式的引數。首先我們要提取每一個單獨的物件:

const obj1: ILink = { url: "string1" };
const obj2: ILink = { url: "string2" };
const obj3: ILink = { url: "string3" };
複製程式碼

接下來我們可以像這樣定義一個 ILink 陣列:

const arrOfLinks: Array<ILink> = [obj1, obj2, obj3];
複製程式碼

引數 searchTerm 對應的型別可以這樣:

const term: string = "java";
複製程式碼

以下是完整的程式碼:

interface ILink {
  description?: string;
  id?: number;
  url: string;
}

function filterByTerm(input: Array<ILink>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm 不能為空");
  if (!input.length) throw Error("input 不能為空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

const obj1: ILink = { url: "string1" };
const obj2: ILink = { url: "string2" };
const obj3: ILink = { url: "string3" };

const arrOfLinks: Array<ILink> = [obj1, obj2, obj3];

const term: string = "java";

filterByTerm(arrOfLinks, term);
複製程式碼

與 JS 相比,TypeScript看起來更冗長,有時甚至是多餘的。但是隨著時間的推移,會發現新增的型別越多,程式碼就越健壯。

通過新增型別註釋,對 TypeScript 的瞭解也越多,還可以幫助你更好地理解程式碼的意圖。

例如,arrOfLinks 與正確的型別(ILink的陣列)相關聯,我們們編輯器就可以推斷出陣列中的每個物件都有一個名為url的屬性,如介面 ILink 中所定義:

【TS 演化史 -- 破曉】一步一個腳印帶你入門 TS

除了字串、陣列和數字之外,TypeScript 還有更多型別。有 booleantuple (元組)any, neverenum 。如果你感興趣,可以檢視文件

現在,我們們繼續擴充套件介面。

擴充套件介面

TypeScript 介面很好。但是,如果哪天我們們需要一個新的物件,所需的型別跟現在有介面基本差不多。假設我們需要一個名為 IPost 的新介面,它具有以下屬性:

  • id, number
  • title, string
  • body, string
  • url, string
  • description, string

該介面的欄位其中有些,我們 ILink 介面都有了。

interface ILink {
  description?: string;
  id?: number;
  url: string;
}
複製程式碼

是否有辦法重用介面 ILink ? 在 TypeScript 中,可以使用繼承來擴充套件介面,關鍵字用 extends 表示:

interface ILink {
  description?: string;
  id?: number;
  url: string;
}

interface IPost extends ILink {
  title: string;
  body: string;
}
複製程式碼

現在,IPost 型別的物件都將具有可選的屬性 descriptionidurl和必填的屬性 titlebody

interface ILink {
  description?: string;
  id?: number;
  url: string;
}

interface IPost extends ILink {
  title: string;
  body: string;
}

const post1: IPost = {
  description:
    "TypeScript tutorial for beginners is a tutorial for all the JavaScript developers ...",
  id: 1,
  url: "www.valentinog.com/typescript/",
  title: "TypeScript tutorial for beginners",
  body: "Some stuff here!"
};
複製程式碼

當像 post1 這樣的物件使用一個介面時,我們說 post1 實現了該介面中定義的屬性。

擴充套件介面意味著借用其屬性並擴充套件它們以實現程式碼重用。當然 TypeScript 介面還也可以描述函式,稍後會看到。

索引

JS 物件是鍵/值對的容器。 如下有一個簡單的物件:

const paolo = {
  name: "Paolo",
  city: "Siena",
  age: 44
};
複製程式碼

我們可以使用點語法訪問任何鍵的值:

console.log(paolo.city);
複製程式碼

現在假設鍵是動態的,我們可以把它放在一個變數中,然後在括號中引用它

const paolo = {
  name: "Paolo",
  city: "Siena",
  age: 44
};

const key = "city";

console.log(paolo[key]);
複製程式碼

現在我們們新增另一個物件,將它們都放到一個陣列中,並使用filter方法對陣列進行篩選,就像我們在 filterByTerm.js 中所做的那樣。但這一次是動態傳遞的,因此可以過濾任何物件

const paolo = {
  name: "Paolo",
  city: "Siena",
  age: 44
};

const tom = {
  name: "Tom",
  city: "Munich",
  age: 33
};

function filterPerson(arr, term, key) {
  return arr.filter(function(person) {
    return person[key].match(term);
  });
}

filterPerson([paolo, tom], "Siena", "city");
複製程式碼

這是比較重要的一行行:

return person[key].match(term);
複製程式碼

能行嗎 是的,因為 JS 不在乎 paolotom 是否可通過動態 [key] 進行“索引化”。 那在 TS 又是怎麼樣的呢?

在下一部分中,我們將使用動態鍵使 filterByTerm 更加靈活。

介面可以有索引

讓我們回到 filterByTerm.tsfilterByTerm 函式

function filterByTerm(input: Array<ILink>, searchTerm: string) {
  if (!searchTerm) throw Error("searchTerm 不能為空");
  if (!input.length) throw Error("input 不能為空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}
複製程式碼

它看起來不那麼靈活,因為對於每個 ILink,我們們都使用硬編碼方式將屬性 url 與正規表示式相匹配。我們希望使動態屬性(也就是鍵)讓程式碼更靈活:

function filterByTerm(
  input: Array<ILink>,
  searchTerm: string,
  lookupKey: string = "url"
) {
  if (!searchTerm) throw Error("searchTerm 不能為空");
  if (!input.length) throw Error("input 不能為空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement[lookupKey].match(regex);
  });
}
複製程式碼

lookupKey 是動態鍵,這是給它分配了預設引數 “url”。 接著編譯程式碼:

npm run tsc
複製程式碼

當然會報錯:

error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'ILink'.
  No index signature with a parameter of type 'string' was found on type 'ILink'.
複製程式碼

出錯行:

return arrayElement[lookupKey].match(regex);
複製程式碼

元素隱式具有 "any" 型別,因為型別 “ILink” 沒有索引簽名,需要你新增一個索引到物件的介面,這很容易解決。

轉到介面 ILink 並新增索引:

interface ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string] : string
}
複製程式碼

語法有點奇怪,但類似於物件上的動態鍵訪問。這意味著我們可以通過型別為 string 的索引訪問該物件的任何鍵,該索引反過來又返回另一個字串。

不過,這樣寫會引發其它錯誤:

error TS2411: Property 'description' of type 'string | undefined' is not assignable to string index type 'string'.
error TS2411: Property 'id' of type 'number | undefined' is not assignable to string index type 'string'.
複製程式碼

這是因為介面上的一些屬性是可選的,可能是 undefined,而且返回型別不總是string(例如,id 是一個 number)。

interface ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string]: string | number | undefined;
}
複製程式碼

這一行:

[index: string]: string | number | undefined;
複製程式碼

表示該索引是一個字串,可以返回另一個字串、數字或 undefined。嘗試再次編譯,這裡有另一個錯誤

error TS2339: Property 'match' does not exist on type 'string | number'.
return arrayElement[lookupKey].match(regex);
複製程式碼

報的沒毛病。match 方法只存在字串中 ,而且我們的索引有可能返回一個 number。為了修正這個錯誤,我們可以使用 any 型別:

interface ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string]: any;
}
複製程式碼

再次編譯通過。

函式的返回型別

到目前為止有很多新東西。現在來看看 TypeScript 的另一個有用的特性**:函式的返回型別**。

回到 filterByTerm 函式:

function filterByTerm(
  input: Array<ILink>,
  searchTerm: string,
  lookupKey: string = "url"
) {
  if (!searchTerm) throw Error("searchTerm 不能為空");
  if (!input.length) throw Error("input 不能為空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement[lookupKey].match(regex);
  });
}
複製程式碼

如果按原樣呼叫,傳遞前面看到的 ILink 陣列和搜尋詞string3,則如預期的那樣返回一個物件陣列:

filterByTerm(arrOfLinks, "string3"); 

// EXPECTED OUTPUT:
// [ { url: 'string3' } ]
複製程式碼

但現在考慮一個更改的變體:

function filterByTerm(
  input: Array<ILink>,
  searchTerm: string,
  lookupKey: string = "url"
) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input
    .filter(function(arrayElement) {
      return arrayElement[lookupKey].match(regex);
    })
    .toString();
}
複製程式碼

如果現在呼叫,使用相同的 ILink 陣列和搜尋詞 string3,它將返回 [object object]

filterByTerm(arrOfLinks, "string3");

// WRONG OUTPUT:
// [object Object]
複製程式碼

該函式沒有按照預期工作,如果對 JS 隱式型別轉換不清楚就很難發現問題。幸運的是,TypeScript 可以捕獲這些錯誤,就像你在編輯器中寫的那樣。

修正如下:

function filterByTerm(/* 省略 */): Array<ILink> {
 /* 省略 */
}
複製程式碼

它是如何工作的? 通過在函式體之前新增型別註釋,告訴 TypeScript 期望另一個陣列作為返回值。現在這個bug 很容易被發現。

interface ILink {
  description?: string;
  id?: number;
  url: string;
  [index: string]: any;
}

function filterByTerm(
  input: Array<ILink>,
  searchTerm: string,
  lookupKey: string = "url"
): Array<ILink> {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!input.length) throw Error("input cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return input
    .filter(function(arrayElement) {
      return arrayElement[lookupKey].match(regex);
    })
    .toString();
}

const obj1: ILink = { url: "string1" };
const obj2: ILink = { url: "string2" };
const obj3: ILink = { url: "string3" };

const arrOfLinks: Array<ILink> = [obj1, obj2, obj3];

filterByTerm(arrOfLinks, "string3");
複製程式碼

現在編譯並檢查錯誤:

error TS2322: Type 'string' is not assignable to type 'ILink[]'.
複製程式碼

我們們希望返回值的是 ILink 陣列,而不是字串。要修復此錯誤,從末尾刪除 .tostring() 並重新編譯程式碼就行了。

型別別名 vs 介面

到目前為止,我們已經將介面視為描述物件和自定義型別的工具。但是通過其他人的程式碼,你可能也注意到了關鍵字的 type

顯然,interface 和 type 在 TypeScript 中可以互換使用,但是它們在許多方面有所不同,這就是TypeScript 給初學者的困惑。

請記住: TypeScript 中的介面描述是某個東西的結構,大多數情況下是一個複雜的物件。

另一方面,type 也可以用來描述自定義的結構,但它只是一個別名,或者換句話說,是自定義型別的標籤。例如,設想一個有兩個欄位的介面,其中一個是布林型、數字型和字串型的聯合型別

interface IExample {
  authenticated: boolean | number | string;
  name: string;
}
複製程式碼

例如,使用 type 別名 可以提取自定義聯合型別,並建立名為 Authenticated 的標籤

type Authenticated = boolean | number | string;

interface IExample {
  authenticated: Authenticated;
  name: string;
}
複製程式碼

通過這種方式,我們可以隔離所做的更改,就不必在整個程式碼庫中複製/貼上 聯合型別

如果要將 type 應用上面示例(filterByTerm),建立一個名為 ILinks 的新標籤,並將 Array 分配給它。 這樣,就可以引用前者:

// the new label
type ILinks = Array<ILink>;
// the new label

function filterByTerm(
  input: ILinks,
  searchTerm: string,
  lookupKey: string = "url"
): ILinks {
  if (!searchTerm) throw Error("searchTerm 不能為空");
  if (!input.length) throw Error("input 不能為空");
  const regex = new RegExp(searchTerm, "i");
  return input.filter(function(arrayElement) {
    return arrayElement[lookupKey].match(regex);
  });
}

const obj1: ILink = { url: "string1" };
const obj2: ILink = { url: "string2" };
const obj3: ILink = { url: "string3" };

const arrOfLinks: ILinks = [obj1, obj2, obj3];

filterByTerm(arrOfLinks, "string3");
複製程式碼

當然,這不是 type 用法最好事例。那麼在 interfacetype 之間使用哪個呢? 我更喜歡複雜物件的介面。TypeScript 文件 也建議了。

一個軟體的理想狀態是可以擴充套件,因此,如果可能,應始終在型別別名上使用介面。

更多關於介面和物件的知識點

函式是 JS 中的一等公民,而物件是該語言中最重要的實體。

物件大多是鍵/值對的容器,它們也可以儲存函式,這一點也不奇怪。當一個函式位於一個物件內部時,它可以通過關鍵字 this 訪問“宿主”物件:

const tom = {
  name: "前端小智",
  city: "廈門",
  age: 26,
  printDetails: function() {
    console.log(`${this.name} - ${this.city}`);
  }
};
複製程式碼

到目前為止,我們們已經看到 TypeScript 介面應用於簡單物件,用於描述字串和數字。 但是他們可以做的更多。 舉個例, 使用以下程式碼建立一個名為 interfaces-functions.ts 的新檔案:

const tom = {
  name: "前端小智",
  city: "廈門",
  age: 26,
  printDetails: function() {
    console.log(`${this.name} - ${this.city}`);
  }
};
複製程式碼

這是一個 JS 物件,我們們使用介面 IPerson 給它加上型別:

interface IPerson {
  name: string;
  city: string;
  age: number;
}

const tom: IPerson = {
  name: "前端小智",
  city: "廈門",
  age: 26,
  printDetails: function() {
    console.log(`${this.name} - ${this.city}`);
  }
};
複製程式碼

編譯程式碼並檢視報錯資訊:

interfaces-functions.ts:11:3 - error TS2322: Type '{ name: string; city: string; age: number; printDetails: () => void; }' is not assignable to type 'IPerson'.
  Object literal may only specify known properties, and 'printDetails' does not exist in type 'IPerson'.
複製程式碼

IPerson 沒有任何名為printDetails的屬性,但更重要的是它應該是一個函式。幸運的是,TypeScript 介面也可以描述函式。如下所示:

interface IPerson {
  name: string;
  city: string;
  age: number;
  printDetails(): void;
}
複製程式碼

在這裡,我們新增了型別函式的屬性 printDetails,返回 voidvoid 表示不返回任何值。

實際上,列印到控制檯的函式不會返回任何內容。 如果要從 printDetails 返回字串,則可以將返回型別調整為 string

interface IPerson {
  name: string;
  city: string;
  age: number;
  printDetails(): string;
}

const tom: IPerson = {
  name: "前端小智",
  city: "廈門",
  age: 26,
  printDetails: function() {
    return `${this.name} - ${this.city}`;
  }
};
複製程式碼

如果函式有引數呢? 在介面中,可以為它們新增型別註釋

interface IPerson {
  name: string;
  city: string;
  age: number;
  printDetails(): string;
  anotherFunc(a: number, b: number): number;
}
複製程式碼

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

原文:www.valentinog.com/blog/typesc…

總結

這裡無法涵蓋每一個 TypeScript 特性。例如,省略了ES2015類及其與介面或更高階型別(如 Partial )的關係。當然後續會持續介紹。

在這個 TypeScript 教程中,講了:

  • 變數,函式引數和返回值的型別註釋
  • 介面
  • 自定義型別
  • 型別別名

TS 幫助我們們減少一些 JS 程式碼隱藏的錯誤。需要重複的是,TypeScript 不能替代測試。 蛤它確實是一個有價值的工具,一開始很難掌握,但完全值得投資。

交流

乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。

github.com/qq449245884…

因為篇幅的限制,今天的分享只到這裡。如果大家想了解更多的內容的話,可以去掃一掃每篇文章最下面的二維碼,然後關注我們們的微信公眾號,瞭解更多的資訊和有價值的內容。

clipboard.png

相關文章