一文學懂 TypeScript 的型別

前端先鋒發表於2019-03-18

翻譯:瘋狂的技術宅

原文:2ality.com/2018/04/typ…

你將學到什麼

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

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 有一個線上執行環境。為了得到最全面的資訊,你應該在 “Options” 選單中開啟所有選項開關。這相當於在 --strict 模式下執行TypeScript編譯器。

關於型別檢查的詳細說明

我在用 TypeScript 時總是喜歡開啟 --strict 開關設定。沒有它,程式可能會稍微好寫一點,但是你也失去了靜態型別檢查的好處。目前此設定能夠開啟以下子設定:

  • --noImplicitAny:如果 TypeScript 無法推斷型別,則必須指定它。這主要用於函式和方法的引數:使用此設定,你必須對它們進行註釋。
  • --noImplicitThis:如果 this 的型別不清楚則會給出提示資訊。
  • --alwaysStrict:儘可能使用 JavaScript 的嚴格模式。
  • --strictNullChecksnull 不屬於任何型別(除了它自己的型別,null),如果它是可接受的值,則必須明確指定。
  • --strictFunctionTypes:對函式型別更加嚴格的檢查。
  • --strictPropertyInitialization:如果屬性的值不能是 undefined ,那麼它必須在建構函式中進行初始化。

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

型別

在本文中,我們把型別看作是一組值的集合。 JavaScript 語言(不是TypeScript!)有7種型別:

  • Undefined:具有唯一元素 undefined 的集合。
  • Null:具有唯一元素“null”的集合。
  • Boolean:具有兩個元素 falsetrue 的集合。
  • Number:所有數字的集合。
  • String:所有字串的集合。
  • Symbol:所有符號的集合。
  • Object:所有物件的集合(包括函式和陣列)。

所有這些型別都是 dynamic:可以用在執行時。

TypeScript 為 JavaScript 帶來了額外的層:靜態型別。這些僅在編譯或型別檢查原始碼時存在。每個儲存位置(變數或屬性)都有一個靜態型別,用於預測其動態值。型別檢查可確保這些預測能夠實現。還有很多可以進行 靜態 檢查(不執行程式碼)的東西。例如,如果函式 f(x) 的引數 x 是靜態型別 number,則函式呼叫 f('abc') 是非法的,因為引數 'abc' 是錯誤的靜態型別。

型別註釋

變數名後的冒號開始 型別註釋:冒號後的型別簽名用來描述變數可以接受的值。例如以程式碼告訴 TypeScript 變數 “x” 只能儲存數字:

let x: number;
複製程式碼

你可能想知道用 undefined 去初始化 x 是不是違反了靜態型別。 TypeScript 不會允許這種情況出現,因為在為它賦值之前不允許操作 x

型別推斷

即使在 TypeScript 中每個儲存位置都有靜態型別,你也不必總是明確的去指定它。 TypeScript 通常可以對它的型別進行推斷。例如如果你寫下這行程式碼:

let x = 123;
複製程式碼

然後 TypeScript 會推斷出 x 的靜態型別是 number

型別描述

在型別註釋的冒號後面出現的是所謂的型別表示式。這些範圍從簡單到複雜,並按如下方式建立。

基本型別是有效的型別表示式:

  • 對應 JavaScript 動態型別的靜態型別:

    • undefined, null

    • boolean, number, string

    • symbol

    • object

    • 注意:值 undefined 與型別 undefined(取決於所在的位置)

  • TypeScript 的特定型別:

    • Array(從技術上講不是 JS 中的型別)
    • any(所有值的型別)
    • 等等其他型別

請注意,“undefined作為值“ 和 ”undefined作為型別” 都寫做 undefined。根據你使用它的位置,被解釋為值或型別。 null 也是如此。

你可以通過型別運算子對基本型別進行組合的方式來建立更多的型別表示式,這有點像使用運算子 union)和intersection()去合併集合。

下面介紹 TypeScript 提供的一些型別運算子。

陣列型別

陣列在 JavaScript 中扮演以下兩個角色(有時是兩者的混合):

  • 列表:所有元素都具有相同的型別。陣列的長度各不相同。
  • 元組:陣列的長度是固定的。元素不一定具有相同的型別。

陣列作為列表

陣列 arr 被用作列表有兩種方法表示 ,其元素都是數字:

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

通常如果存在賦值的話,TypeScript 就可以推斷變數的型別。在這種情況下,實際上你必須幫它解決型別問題,因為在使用空陣列時,它無法確定元素的型別。

稍後我們將回到尖括號表示法(Array<number>)。

陣列作為元組

如果你想在陣列中儲存二維座標點,那麼就可以把這個陣列當作元組去用。看上去是這個樣子:

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

在這種情況下,你不需要型別註釋。

另外一個例子是 Object.entries(obj) 的返回值:一個帶有一個 [key,value] 對的陣列,它用於描述 obj 的每個屬性。

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

Object.entries() 的返回值型別是:

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

函式型別

以下是函式型別的例子:

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

這個型別是一個函式,它接受一個數字型別引數並且返回值為字串。在型別註釋中使用這種型別(String 在這裡是個函式)的例子:

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

同樣,我們一般不會在這裡使用型別註釋,因為 TypeScript 知道 String 的型別,因此可以推斷出 func 的型別。

以下程式碼是一個更實際的例子:

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

由於我們使用了函式型別來描述 stringify123() 的引數 callback,所以TypeScript 拒絕以下函式呼叫。

f(Number);
複製程式碼

但它接受以下函式呼叫:

f(String);
複製程式碼

函式宣告的返回型別

對函式的所有引數進行註釋是一個很好的做法。你還可以指定返回值型別(不過 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 時,如果事先檢查時發現 callback 沒有被省略,它只允許你在 A 行進行函式呼叫。

引數預設值

TypeScript支援 ES6 引數預設值

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

預設值可以使引數可選。通常可以省略型別註釋,因為 TypeScript 可以推斷型別。例如它可以推斷出 xy 都是 number 型別。

如果要新增型別註釋,應該這樣寫:

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

rest 型別

你還可以用 ES6 rest operator 進行 TypeScript 引數定義。相應引數的型別必須是陣列:

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

Union

在JavaScript中,有時候變數會是有幾種型別之中的一種。要描述這些變數,可以使用 union types。例如,在下面的程式碼中,xnull 型別或 number 型別:

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

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

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

型別表示式 s | t 的結果是型別 st 在集合理論意義上的聯合(正如我們之前看到的那樣,兩個集合)。

下面讓我們重寫函式 stringify123():這次我們不希望引數 callback 是可選的。應該總是呼叫它。如果呼叫者不想傳入一個函式,則必須顯式傳遞 null。實現如下。

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

請注意,在行 B 進行函式呼叫之前,我們必須再次檢查 callback 是否真的是一個函式(行A)。如果沒有檢查,TypeScript 將會報告錯誤。

Optional 與 undefined|T

型別為 T 的可選引數和型別為 undefined|T 的引數非常相似。 (另外對於可選屬性也是如此。)

主要區別在於你可以省略可選引數:

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

But you can’t omit parameters of type 但是你不能省略 undefined|T 型別的引數:

function f2(x: undefined | number) { }
f2(); // error
f2(undefined); // OK
f2(123); // OK
複製程式碼

nullundefined 通常不包含在型別中

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

相反,在TypeScript中,undefinednull 由單獨的不相交型別處理。如果你想使它們生效,必須要有一個型別聯合,如 undefined|stringnull|string

物件

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

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

我們將在本文章中忽略 object-as-dictionaries。順便說一句,無論如何,map 通常是比字典的更好選擇。

通過介面描述 objects-as-records

介面描述 objects-as-records 。例如:

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

TypeScript 型別系統的一大優勢在於它的結構上,而不是在命名上。也就是說,介面 Point 能夠匹配適當結構的所有物件:

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

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

可選屬性

如果可以省略屬性,則在其名稱後面加上一個問號:

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

方法

介面內還可以包含方法:

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

型別變數和泛型型別

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

  • 值存在於物件級別
  • 型別存在於元級別

同理:

  • 普通變數定義在物件級別之上。
  • 型別變數存在於元級別之上。它們是值為型別的變數。

普通變數通過 constlet 等引入。型別變數通過尖括號( <> )引入。例如以下程式碼包含型別變數 T,通過 <T> 引入。

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

你可以看到型別引數 TStack 的主體內出現兩次。因此,該介面可以直觀地理解如下:

  • Stack 是一堆值,它們都具有給定的型別 T。每當你提到 Stack 時,必須寫 T。接下來我們會看到究竟該怎麼用。
  • 方法 .push() 接受型別為 T 的值。
  • 方法 .pop() 返回型別為 T 的值。

如果使用 Stack,則必須為 T 指定一個型別。以下程式碼顯示了一個虛擬棧,其唯一目的是匹配介面。

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

例子:map

map 在 TypeScript 中的定義。例如:

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);
}
複製程式碼

型別變數 T 在這段程式碼中出現三次:

  • fillArray<T>:引入型別變數
  • elem:T:使用型別變數,從引數中選擇它。
  • Array<T>:將 T 傳遞給 Array 的建構函式。

這意味著:我們不必顯式指定Array<T>的型別 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;
  ···
}
複製程式碼

這是一個Array的介面,其元素型別為 T,每當使用這個介面時必須填寫它:

  • 方法.concat()有零個或多個引數(通過 rest 運算子定義)。其中每一個引數中都具有型別 T[]|T。也就是說,它是一個 T 型別的陣列或是一個 T 值。
  • 方法.reduce() 引入了自己的型別變數 UU 表示以下實體都具有相同的型別(你不需要指定,它是自動推斷的):
    • Parameter state of callback() (which is a function)
  • statecallback() 的引數(這是一個函式)
  • Result of callback()
  • callback()的返回
  • .reduce()的可選引數 firstState
  • Result of .reduce()
  • .reduce()的返回

callback 還將獲得一個 element 引數,其型別與 Array 元素具有相同的型別 T,引數 index 是一個數字,引數 arrayT 的值。

擴充套件閱讀

歡迎關注京程一燈公眾號:【京程一燈】,獲取更多前端乾貨。

相關文章