理解 TypeScript 的靜態型別

hi_chegde發表於2018-04-20

這篇博文快速介紹 TypeScript 靜態型別的標註。

你將學習什麼

閱讀本文後,您應該能夠理解以下程式碼的含義。

interface Array<T> {
  concat(...items: Array<T[] | T>): T[];
  reduce<U>(
    callback: (state: U, element: T, index: number, array: T[]) => U,
    firstState?: U): U;
  ···
}
複製程式碼

如果你認為這很神祕,那麼我同意你的看法。 但是(正如我希望證明的),這個標註相對來說容易學習。 一旦你理解了它,它會告訴你,一個直接,精確和全面的總結這些程式碼行為。 無需閱讀英文長篇描述。

嘗試一下程式碼示例

TypeScript 有一個線上編譯。 為了獲得最全面的檢查,應該開啟 “option” 選單中的所有內容。 這相當於在 --strict 模式下執行TypeScript編譯器。

設定全部的型別檢查

我總是使用 TypeScript 以最全面的設定 --strict。 沒有它,程式的編寫會變得相對容易些,但是你也會失去靜態型別檢查的許多好處。 目前,此設定啟用以下子設定:

  • --noImplicitAny: 如果 TypeScript 無法推斷出某種型別,說明你沒有啟用這個選項。 這主要適用於函式和方法的引數:使用此設定,你就必須標註它們的型別。
  • --noImplicitThis: 如果這個型別不明確,提示警告。
  • --alwaysStrict: 儘可能使用 JavaScript 嚴格模式。
  • --strictNullChecks: null 不是任何型別的一部分(除了它自己的型別,null),並且如果它是可接受的值,則必須明確給值。
  • --strictFunctionTypes: 強化函式型別檢查。
  • --strictPropertyInitialization: 如果某個屬性的值未定義,那麼它必須在建構函式中初始化。

更多資訊:TypeScript手冊中的“編譯器選項”一章。

型別

在這篇博文中,型別是一組值。 JavaScript 語言(不是TypeScript!)有7種型別:

  • Undefined: 元素為未定義的集合。
  • Null: 元素為 null 的集合。
  • Boolean: 元素為 false 或 true 的集合。
  • Number: 元素為數字的集合。
  • String: 元素為字串的集合。
  • Symbol: 元素為 symbols 的集合。
  • Object: 元素為物件(包括函式和陣列)的集合.

所有這些型別都是動態的:您可以在執行時使用它們。

TypeScript 在 JavaScript 基礎上增加了一層:靜態型別。它只存在於編譯的時候或者原始碼型別檢查的時候。每一個有靜態型別的儲存(變數或者屬性)地方都可以預知它的值。型別檢查確保實現型別推斷,不用執行程式碼就能進行靜態型別檢查。舉個例子,如果函式f(x)的引數x具有靜態數字型別,則函式呼叫f('abc')是非法的,因為引數'abc'是錯誤的靜態型別引數。

型別標註

型別標註在變數的冒號後面。冒號後面的靜態型別標註描述了這個變數可以有什麼值。下面一個例子表示這個變數只能儲存數字型別。

let x: number;
複製程式碼

你可能想知道如果 x 沒有被初始化的時候是不是可以通過靜態型別檢查。對於這個問題, TypeScript 在給它賦值之前不會讓你讀取 x 。

型別推斷

在 TypeScript 中,即使所有變數你都寫靜態型別,但你不需要全部明確寫它的靜態型別。 TypeScript 經常可以推斷出它。 例如,如果你寫

let x = 123;
複製程式碼

然後TypeScript推斷x具有數字靜態型別。

描述型別

型別表示式在型別標註的冒號之後出現。 這些範圍從簡單到複雜,建立如下

合法的型別表示式有基本型別:

  • JavaScript 動態型別的靜態型別:undefined,null,boolean,number,string,symbol,object
  • TypeScript 特定型別:any(所有值的型別)等

注意,“未定義為值”和“未定義為型別”都被定義為未定義。 根據你使用它的地方,它作為一個值或者一個型別。 null也是一樣。

可以通過型別運算子組合基本型別來建立更多型別表示式,型別運算子與運算子 union(∪) 和 intersection(∩) 組合類似。

接下來解釋 TypeScript 提供的一些型別運算子。

陣列作為列表

有兩種方法可以表示 Array arr 其元素都是數字的列表:

let arr: number[] = [];
let arr: Array<number> = [];
複製程式碼

通常情況下,如果有給值,TypeScript可以推斷變數的型別。 在這種情況下,你實際上必須明確標註型別,因為對於空陣列,它無法確定元素的型別。

稍後,講解尖括號表示法(Array)。

陣列作為元組

如果您在陣列中儲存兩個點,那麼你將該陣列用作元組。 這看起來如下:

let point: [number, number] = [7, 5];
複製程式碼

在這種情況下,不需要型別註釋。 元組的另一個例子是 Object.entries(obj) 的結果:對於 obj 的每個屬性都有一個 [key,value] 對的陣列。

> Object.entries({a:1, b:2})
[ [ 'a', 1 ], [ 'b', 2 ] ]
複製程式碼

Object.entries() 的型別是:

Array<[string, any]>
複製程式碼

函式型別

這是一個函式型別的例子:

(num: number) => string
複製程式碼

該型別接受單個數字型別引數,和返回字串型別的值。 讓我們在型別註釋中使用這個型別(字串在這裡用作函式):

const func: (num: number) => string = String;
複製程式碼

同樣,我們通常不會在這裡使用型別註釋,因為 TypeScript 知道 String 的型別,因此可以推斷出 func 的型別。 下面的程式碼是一個更實用的例子:

function stringify123(callback: (num: number) => string) {
  return callback(123);
}
複製程式碼

我們使用函式型別來描述 stringify123() 的回撥函式。 由於此型別註釋,TypeScript 拒絕以下函式呼叫。

f(String);
複製程式碼

但它接受以下函式呼叫:

f(Number);
複製程式碼

(原文在這可能順序寫反了)

函式結果型別宣告

標註函式的所有引數型別是一種很好的實踐。 你也可以指定結果型別(但 TypeScript 很適合推斷它):

function stringify123(callback: (num: number) => string): string {
  const num = 123;
  return callback(num);
}
複製程式碼

特殊結果型別 void

void 是函式結果的特殊型別:它告訴 TypeScript 函式總是返回 undefined (顯式或隱式):

function f1(): void { return undefined } // OK
function f2(): void { } // OK
function f3(): void { return 'abc' } // error
複製程式碼

可選引數

識別符號後面的問號表示該引數是可選的。 例如:

function stringify123(callback?: (num: number) => string) {
  const num = 123;
  if (callback) {
    return callback(num); // (A)
  }
  return String(num);
}
複製程式碼

如果您在 --strict 模式下執行 TypeScript ,則會在檢查回撥沒有被省略的情況下讓你在 A 行中進行函式呼叫。

引數預設值

TypeScript 支援 ES6引數預設值

function createPoint(x=0, y=0) {
  return [x, y];
}
複製程式碼

預設值使引數可選。 通常可以省略型別註釋,因為 TypeScript 可以推斷型別。 例如,它可以推斷x和y都具有數字型別。

如果你想新增型別註釋,那看起來如下。

function createPoint(x:number = 0, y:number = 0) {
  return [x, y];
}
複製程式碼

剩餘型別

您還可以使用 ES6 rest 運算子來處理 TypeScript 引數定義。 相應引數的型別必須是Array:

function joinNumbers(...nums: number[]): string {
    return nums.join('-');
}
joinNumbers(1, 2, 3); // '1-2-3'
複製程式碼

聯合型別

在JavaScript中,變數常常是幾種型別之一。 要描述這些變數,你可以使用聯合型別。 例如,在以下程式碼中,x 的型別為 null 或型別 number :

let x = null;
x = 123;
複製程式碼

x 的型別可以被描述為 null | number :

let x: null|number = null;
x = 123;
複製程式碼

型別表示式 s | t 的結果是 型別s 和 t 的集合論聯合(正如我們前面所看到的那樣,這兩個集合)。

讓我們重寫函式 stringify123() :這一次,我們不希望引數回撥是可選的。 應該總是傳遞該引數。 如果呼叫者不想提供函式,他們必須顯式傳遞null。 這是實現如下:

function stringify123(
  callback: null | ((num: number) => string)) {
  const num = 123;
  if (callback) { // (A)
    return callback(123); // (B)
  }
  return String(num);
}
複製程式碼

注意,實際上我們必須檢查回撥是否是一個函式(A行),然後才能在B行進行函式呼叫。如果沒有檢查,TypeScript將報告錯誤。

? 與 undefined| T

型別T的可選引數? 和型別為 undefined | T 的引數非常相似。 (順便說一句,對於可選屬性也是如此。)

主要區別是可以省略可選引數:

function f1(x?: number) { }
f1(); // OK
f1(undefined); // OK
f1(123); // OK
複製程式碼

但是你不能省略型別為 undefined| T 的引數:

function f1(x?: number) { }
f1(); // OK
f1(undefined); // OK
f1(123); // OK
複製程式碼

在型別中,通常不包含 null 和 undefined 值

在許多程式語言中, null 是所有型別的一部分。 例如,在 java 中,只要引數的型別是 String ,就可以傳遞 null 並且 Java 不會報錯

相比之下,在 TypeScript 中,undefined 和 null 由不同的型別處理。 如果你想跟上述一樣,你需要一個型別聯合,比如 undefined | number 和 null | number 。

物件型別

與陣列類似,物件在 JavaScript 中扮演兩個角色( 偶爾混合 和 或更動態 ):

  • 記錄:開發時已知的固定數量的屬性。 每個屬性可以有不同的型別。
  • 字典:在開發時不知道名稱的任意數量的屬性。 所有屬性鍵(字串和/或符號)具有相同的型別,屬性值也是如此。

我們將在此文中忽略物件作為詞典。 另外,無論如何,Maps 對於字典來說通常是更好的選擇。

通過 interfaces 描述物件型別作為記錄

interfaces 描述物件型別。 例如:

interface Point {
  x: number;
  y: number;
}
複製程式碼

TypeScript的型別系統的一大優點是它是結構化的,而不是字面化。 也就是說, interface 匹配具有適當結構的所有物件:

function pointToString(p: Point) {
  return `(${p.x}, ${p.y})`;
}
pointToString({x: 5, y: 7}); // '(5, 7)'
複製程式碼

相比之下,Java的型別系統需要類來實現介面。

可選屬性

如果一個屬性可以被省略,你在變數後面加一個問號:

interface Person {
  name: string;
  company?: string;
}
複製程式碼

函式

Interfaces 也可包含函式:

interface Point {
  x: number;
  y: number;
  distance(other: Point): number;
}
複製程式碼

型別變數和泛型型別

使用靜態型別,有兩個級別:

  • 物件級別
  • 元型別級別

相似於

  • 物件儲存正常變數
  • 描述型別的變數,也是值型別的變數

正常變數通過 const,let 等引入。 型別變數通過尖括號(<>)引入。 例如,以下程式碼包含通過引入的型別變數T.

interface Stack<T> {
  push(x: T): void;
  pop(): T;
}
複製程式碼

你可以看到型別 引數T 在 Stack中 出現兩次。 所以這個介面可以直觀地理解如下:

  • Stack 是一堆所有具有給定 型別T 的值。每當提到 Stack 時,您必須填寫T. 接下來我們將看到如何。
  • push 函式 接受 T型別引數
  • pop 函式 返回 T型別值

如果你使用 Stack ,你就需要給定 T的值。 以下程式碼顯示了一個虛介面 Stack ,其唯一目的是匹配介面。

const dummyStack: Stack<number> = {
  push(x: number) {},
  pop() { return 123 },
};
複製程式碼

示例:Maps

TypeScript 定義 Map 型別。例如:

const myMap: Map<boolean,string> = new Map([
  [false, 'no'],
  [true, 'yes'],
]);
複製程式碼

泛型函式

函式(和方法)也可以引入型別變數:

function id<T>(x: T): T {
  return x;
}
複製程式碼

使用如下

id<number>(123);
複製程式碼

由於型別推理,可以省略型別引數

id(123);
複製程式碼

型別引數的傳遞

function fillArray<T>(len: number, elem: T) {
  return new Array<T>(len).fill(elem);
}
複製程式碼

不必顯式指定 Array 的型別T - 它是從引數 elem 推斷出來的:

const arr = fillArray(3, '*');
  // Inferred type: string[]
複製程式碼

結論

讓我們用我們所學的知識來理解我們之前看到的那段程式碼:

interface Array<T> {
  concat(...items: Array<T[] | T>): T[];
  reduce<U>(
    callback: (state: U, element: T, index: number, array: T[]) => U,
    firstState?: U): U;
  ···
}
複製程式碼

這是一個陣列的 interface ,其元素的型別為 T ,我們必須在使用此 interface 時填寫它們:

方法 .concat() 有零個或更多引數(通過剩餘引數操作符)。他們中的每個函式有型別 T[] 或 T 。因此,它可能是型別T 的陣列或者是單個 T 的值。

方法 .reduce() 引進了它自己的型別變數,U 。 U 表示以下實體全都具有相同型別(不需要指定,它會自動推斷):

  • 回撥函式裡的 state 引數
  • 回撥函式裡的結果
  • 方法 .reduce() 的可選引數 firstState
  • .reduce() 的結果

回撥函式也獲取一個引數 element ,其型別與陣列元素的 型別T 相同,引數 index 是數字,引數 array 是 T值 。

進一步閱讀

  • 書 (線上免費閱讀): “Exploring ES6
  • ECMAScript Language Types” 在 ECMAScript 規範.
  • TypeScript Handbook”: 一本寫的很好解釋了 TypeScript 支援的多種型別及型別操作。
  • TypeScript 倉庫裡有完整 ECMAScript 標準庫的型別定義。練習型別標註的簡單方法是閱讀它們。

本文翻譯自原文 - Understanding TypeScript’s type notation -> star

由於本人水平有限,錯誤之處在所難免,敬請指正!

轉載請註明出處,保留原文連結以及作者資訊。

相關文章