處理 TypeScript
請支持這本書:購買捐款
(廣告,請不要封鎖。)

7 TypeScript 的精髓



本章說明 TypeScript 的基本概念。

7.1 您將學到什麼

閱讀完本章後,您應該能夠了解以下 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;
  // ···
}

您可能會覺得這很神秘。我同意您的看法!但(我希望證明)這個語法相對容易學習。一旦您了解它,它就能立即、精確且全面地摘要程式碼的行為,而無需閱讀冗長的英文說明。

7.2 指定型別檢查的全面性

有許多方法可以設定 TypeScript 編譯器。其中一組重要的選項控制編譯器徹底檢查 TypeScript 程式碼的方式。最高設定值是透過 --strict 啟用,我建議總是使用它。它讓程式變得稍微難寫,但我們也能獲得靜態型別檢查的全部好處。

  關於 --strict,您現在只需要知道這些

如果您想進一步了解,請繼續閱讀。

--strict 設定為 true,會將以下所有選項設定為 true

我們將在本書後面介紹更多編譯器選項,屆時我們將使用 TypeScript 建立 npm 套件網路應用程式。TypeScript 手冊有關於它們的 完整文件

7.3 TypeScript 中的型別

在本章中,型別只是一個值集合。JavaScript 語言(非 TypeScript!)只有八種型別

  1. 未定義:只有一個元素 undefined 的集合
  2. Null:只有一個元素 null 的集合
  3. 布林值:有兩個元素 falsetrue 的集合
  4. 數字:所有數字的集合
  5. BigInt:所有任意精度的整數集合
  6. 字串:所有字串的集合
  7. 符號:所有符號的集合
  8. 物件:所有物件的集合(包括函式和陣列)

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

TypeScript 為 JavaScript 帶來額外一層:靜態型別。這些只存在於編譯或類型檢查原始碼時。每個儲存位置(變數、屬性等)都有預測其動態值的靜態型別。類型檢查確保這些預測成真。

而且有很多東西可以在靜態下(不執行程式碼)檢查。例如,如果函式 toString(num) 的參數 num 有靜態型別 number,則函式呼叫 toString('abc') 是非法的,因為參數 'abc' 有錯誤的靜態型別。

7.4 類型註解

function toString(num: number): string {
  return String(num);
}

在先前的函式宣告中有兩個類型註解

numberstring 都是指定儲存位置型別的類型表達式

7.5 類型推論

通常,如果沒有類型註解,TypeScript 可以推論靜態型別。例如,如果我們省略 toString() 的回傳型別,TypeScript 會推論它是 string

// %inferred-type: (num: number) => string
function toString(num: number) {
  return String(num);
}

類型推論並非猜測:它遵循明確的規則(類似於算術),用於推導未明確指定類型的類型。在此情況下,回傳陳述套用函式 String(),將任意值對應到字串,到類型為 number 的值 num,並回傳結果。這就是推論回傳類型為 string 的原因。

如果位置的類型既未明確指定也無法推論,TypeScript 會對其使用類型 any。這是所有值的類型,也是萬用字元,表示如果值具有該類型,我們可以執行任何操作。

使用 --strict 時,僅當我們明確使用 any 時才允許使用。換句話說:每個位置都必須具有明確或推論的靜態類型。在以下範例中,參數 num 既沒有明確類型也沒有推論類型,因此我們會收到編譯時期錯誤

// @ts-expect-error: Parameter 'num' implicitly has an 'any' type. (7006)
function toString(num) {
  return String(num);
}

7.6 透過類型表達式指定類型

類型註解冒號後的類型表達式從簡單到複雜,並以以下方式建立。

基本類型是有效的類型表達式

有許多方法可以組合基本類型以產生新的複合類型。例如,透過類型運算子,其組合類型的方式類似於集合運算子聯集 () 和交集 () 組合集合的方式。我們很快就會看到如何執行此操作。

7.7 兩種語言層級:動態與靜態

TypeScript 有兩種語言層級

我們可以在語法中看到這兩個層級

const undef: undefined = undefined;

請注意,相同的語法 undefined 表示不同的意義,具體取決於是在動態層級還是靜態層級使用。

  嘗試培養對兩種語言層級的認識

這有助於理解 TypeScript。

7.8 類型別名

使用 type,我們可以為現有類型建立一個新名稱(別名)

type Age = number;
const age: Age = 82;

7.9 陣列類型

陣列在 JavaScript 中扮演兩個角色(其中之一或同時扮演兩個角色)

7.9.1 陣列作為清單

有兩種方法可以表達陣列 arr 用作清單,其中所有元素都是數字

let arr1: number[] = [];
let arr2: Array<number> = [];

通常,如果有一個賦值,TypeScript 可以推斷變數的類型。在這種情況下,我們實際上必須協助它,因為對於一個空的陣列,它無法確定元素的類型。

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

7.9.2 陣列作為元組

如果我們在陣列中儲存一個二維點,那麼我們將該陣列用作元組。如下所示

let point: [number, number] = [7, 5];

對於陣列作為元組,需要類型註解,因為對於陣列文字,TypeScript 推斷的是清單類型,而不是元組類型

// %inferred-type: number[]
let point = [7, 5];

元組的另一個範例是 Object.entries(obj) 的結果:一個陣列,其中包含 obj 的每個屬性的 [key, value] 對。

// %inferred-type: [string, number][]
const entries = Object.entries({ a: 1, b: 2 });

assert.deepEqual(
  entries,
  [[ 'a', 1 ], [ 'b', 2 ]]);

推斷的類型是元組陣列。

7.10 函數類型

這是函數類型的範例

(num: number) => string

此類型包含每個接受單一數字類型參數並傳回字串的函數。讓我們在類型註解中使用此類型

const toString: (num: number) => string = // (A)
  (num: number) => String(num); // (B)

通常,我們必須為函數指定參數類型。但在這種情況下,B 行中 num 的類型可以從 A 行的函數類型推斷出來,我們可以省略它

const toString: (num: number) => string =
  (num) => String(num);

如果我們省略 toString 的類型註解,TypeScript 會從箭頭函數推斷一個類型

// %inferred-type: (num: number) => string
const toString = (num: number) => String(num);

這次,num 必須有類型註解。

7.10.1 更複雜的範例

以下範例比較複雜

function stringify123(callback: (num: number) => string) {
  return callback(123);
}

我們使用函數類型來描述 stringify123() 的參數 callback。由於這個類型註解,TypeScript 拒絕以下函數呼叫。

// @ts-expect-error: Argument of type 'NumberConstructor' is not
// assignable to parameter of type '(num: number) => string'.
//   Type 'number' is not assignable to type 'string'.(2345)
stringify123(Number);

但它接受這個函數呼叫

assert.equal(
  stringify123(String), '123');

7.10.2 函數宣告的傳回類型

TypeScript 通常可以推斷函數的傳回類型,但明確指定傳回類型是允許的,偶爾也很有用(至少不會造成任何危害)。

對於 stringify123(),指定傳回類型是可選的,如下所示

function stringify123(callback: (num: number) => string): string {
  return callback(123);
}
7.10.2.1 特殊回傳類型 void

void 是函式的一種特殊回傳類型:它告訴 TypeScript 函式總是會回傳 undefined

它可以明確地這樣做

function f1(): void {
  return undefined;
}

或者它可以隱含地這樣做

function f2(): void {}

不過,這種函式不能明確回傳 undefined 以外的值

function f3(): void {
  // @ts-expect-error: Type '"abc"' is not assignable to type 'void'. (2322)
  return 'abc';
}

7.10.3 選用參數

識別碼後面的問號表示參數是選用的。例如

function stringify123(callback?: (num: number) => string) {
  if (callback === undefined) {
    callback = String;
  }
  return callback(123); // (A)
}

只有在確定 callback 不是 undefined(如果省略參數,它就是 undefined)時,TypeScript 才允許我們在 A 行呼叫函式。

7.10.3.1 參數預設值

TypeScript 支援 參數預設值

function createPoint(x=0, y=0): [number, number] {
  return [x, y];
}

assert.deepEqual(
  createPoint(),
  [0, 0]);
assert.deepEqual(
  createPoint(1, 2),
  [1, 2]);

預設值讓參數變成選用的。我們通常可以省略類型註解,因為 TypeScript 可以推論出類型。例如,它可以推論出 xy 都是 number 類型。

如果我們想加入類型註解,會如下所示。

function createPoint(x:number = 0, y:number = 0): [number, number] {
  return [x, y];
}

7.10.4 剩餘參數

我們也可以在 TypeScript 參數定義中使用 剩餘參數。它們的靜態類型必須是陣列(清單或元組)

function joinNumbers(...nums: number[]): string {
  return nums.join('-');
}
assert.equal(
  joinNumbers(1, 2, 3),
  '1-2-3');

7.11 聯合類型

變數所儲存的值(一次一個值)可能是不同類型的成員。在這種情況下,我們需要一個聯合類型。例如,在以下程式碼中,stringOrNumberstring 類型或 number 類型

function getScore(stringOrNumber: string|number): number {
  if (typeof stringOrNumber === 'string'
    && /^\*{1,5}$/.test(stringOrNumber)) {
      return stringOrNumber.length;
  } else if (typeof stringOrNumber === 'number'
    && stringOrNumber >= 1 && stringOrNumber <= 5) {
    return stringOrNumber
  } else {
    throw new Error('Illegal value: ' + JSON.stringify(stringOrNumber));
  }
}

assert.equal(getScore('*****'), 5);
assert.equal(getScore(3), 3);

stringOrNumber 的類型是 string|number。類型表達式 s|t 的結果是類型 st 的集合論聯集(解釋為集合)。

7.11.1 預設情況下,undefinednull 不包含在類型中

在許多程式語言中,null 是所有物件類型的部分。例如,只要變數的類型在 Java 中是 String,我們就可以將它設定為 null,而 Java 也不會抱怨。

相反地,在 TypeScript 中,undefinednull 由不同的不相交類型處理。如果我們想允許它們,我們需要聯合類型,例如 undefined|stringnull|string

let maybeNumber: null|number = null;
maybeNumber = 123;

否則,我們會收到錯誤

// @ts-expect-error: Type 'null' is not assignable to type 'number'. (2322)
let maybeNumber: number = null;
maybeNumber = 123;

請注意,TypeScript 沒有強迫我們立即初始化(只要我們在初始化變數之前沒有從變數中讀取資料即可)

let myNumber: number; // OK
myNumber = 123;

7.11.2 讓省略明確化

回想一下這個先前的函式

function stringify123(callback?: (num: number) => string) {
  if (callback === undefined) {
    callback = String;
  }
  return callback(123); // (A)
}

讓我們重新撰寫 stringify123(),讓參數 callback 不再是選項:如果呼叫者不想要提供函式,他們必須明確傳遞 null。結果如下所示。

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

assert.equal(
  stringify123(null),
  '123');

// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
assert.throws(() => stringify123());

我們必須再次處理 callback 不是函式的情況(A 行),才能在 B 行中進行函式呼叫。如果我們沒有這樣做,TypeScript 會在該行中報告錯誤。

7.12 選擇性值與預設值與 undefined|T

以下三個參數宣告相當類似

如果參數為選擇性,則可以省略。在這種情況下,它的值為 undefined

function f1(x?: number) { return x }

assert.equal(f1(123), 123); // OK
assert.equal(f1(undefined), undefined); // OK
assert.equal(f1(), undefined); // can omit

如果參數具有預設值,則在參數被省略或設定為 undefined 時,會使用該值

function f2(x = 456) { return x }

assert.equal(f2(123), 123); // OK
assert.equal(f2(undefined), 456); // OK
assert.equal(f2(), 456); // can omit

如果參數具有聯集類型,則無法省略,但我們可以將其設定為 undefined

function f3(x: undefined | number) { return x }

assert.equal(f3(123), 123); // OK
assert.equal(f3(undefined), undefined); // OK

// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
f3(); // can’t omit

7.13 物件型別

與陣列類似,物件在 JavaScript 中扮演兩個角色(偶爾會混淆)

我們在本章中忽略物件作為字典——它們在 §15.4.5「索引簽章:物件作為字典」 中有說明。順帶一提,Map 通常是字典的更好選擇。

7.13.1 透過介面為物件作為記錄進行型別化

介面描述物件作為記錄。例如

interface Point {
  x: number;
  y: number;
}

我們也可以透過逗號分隔成員

interface Point {
  x: number,
  y: number,
}

7.13.2 TypeScript 的結構型別與名目型別

TypeScript 型別系統的一大優點是它以結構方式運作,而不是名目方式。也就是說,介面 Point 符合所有具有適當結構的物件

interface Point {
  x: number;
  y: number;
}
function pointToString(pt: Point) {
  return `(${pt.x}, ${pt.y})`;
}

assert.equal(
  pointToString({x: 5, y: 7}), // compatible structure
  '(5, 7)');

相反地,在 Java 的名目型別系統中,我們必須明確宣告每個類別實作哪些介面。因此,類別只能實作在其建立時間存在的介面。

7.13.3 物件文字型別

物件文字型別是匿名介面

type Point = {
  x: number;
  y: number;
};

物件文字型別的一個好處是它們可以在內嵌中使用

function pointToString(pt: {x: number, y: number}) {
  return `(${pt.x}, ${pt.y})`;
}

7.13.4 選擇性屬性

如果可以省略屬性,我們會在其名稱後加上問號

interface Person {
  name: string;
  company?: string;
}

在以下範例中,johnjane 都符合介面 Person

const john: Person = {
  name: 'John',
};
const jane: Person = {
  name: 'Jane',
  company: 'Massive Dynamic',
};

7.13.5 方法

介面也可以包含方法

interface Point {
  x: number;
  y: number;
  distance(other: Point): number;
}

就 TypeScript 的類型系統而言,方法定義和值為函式的屬性是等效的

interface HasMethodDef {
  simpleMethod(flag: boolean): void;
}
interface HasFuncProp {
  simpleMethod: (flag: boolean) => void;
}

const objWithMethod: HasMethodDef = {
  simpleMethod(flag: boolean): void {},
};
const objWithMethod2: HasFuncProp = objWithMethod;

const objWithOrdinaryFunction: HasMethodDef = {
  simpleMethod: function (flag: boolean): void {},
};
const objWithOrdinaryFunction2: HasFuncProp = objWithOrdinaryFunction;

const objWithArrowFunction: HasMethodDef = {
  simpleMethod: (flag: boolean): void => {},
};
const objWithArrowFunction2: HasFuncProp = objWithArrowFunction;

我的建議是使用最能表達如何設定屬性的語法。

7.14 類型變數和泛型類型

回想 TypeScript 的兩個語言層級

類似地

  命名類型參數

在 TypeScript 中,通常使用單一的大寫字元(例如 TIO)作為類型參數。不過,任何合法的 JavaScript 識別碼都允許使用,而且較長的命名通常能讓程式碼更容易理解。

7.14.1 範例:值的容器

// Factory for types
interface ValueContainer<Value> {
  value: Value;
}

// Creating one type
type StringContainer = ValueContainer<string>;

Value類型變數。可以在尖括號中引入一個或多個類型變數。

7.15 範例:泛型類別

類別也可以有類型參數

class SimpleStack<Elem> {
  #data: Array<Elem> = [];
  push(x: Elem): void {
    this.#data.push(x);
  }
  pop(): Elem {
    const result = this.#data.pop();
    if (result === undefined) {
        throw new Error();
    }
    return result;
  }
  get length() {
    return this.#data.length;
  }
}

類別 SimpleStack 有類型參數 Elem。當我們實例化類別時,我們也提供類型參數的值

const stringStack = new SimpleStack<string>();
stringStack.push('first');
stringStack.push('second');
assert.equal(stringStack.length, 2);
assert.equal(stringStack.pop(), 'second');

7.15.1 範例:Map

Map 在 TypeScript 中是泛型型別。例如

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

由於類型推論(基於 new Map() 的引數),我們可以省略類型參數

// %inferred-type: Map<boolean, string>
const myMap = new Map([
  [false, 'no'],
  [true, 'yes'],
]);

7.15.2 函式和方法的類型變數

函式定義可以像這樣引入類型變數

function identity<Arg>(arg: Arg): Arg {
  return arg;
}

我們使用函式如下。

// %inferred-type: number
const num1 = identity<number>(123);

由於類型推論,我們可以再次省略類型參數

// %inferred-type: 123
const num2 = identity(123);

請注意,TypeScript 推論出類型 123,這是一個只有一個數字的集合,比類型 number 更具體。

7.15.2.1 箭頭函式和方法

箭頭函式也可以有型別參數

const identity = <Arg>(arg: Arg): Arg => arg;

這是方法的型別參數語法

const obj = {
  identity<Arg>(arg: Arg): Arg {
    return arg;
  },
};

7.15.3 更複雜的函式範例

function fillArray<T>(len: number, elem: T): T[] {
  return new Array<T>(len).fill(elem);
}

型別變數 T 在此程式碼中出現四次

我們可以在呼叫 fillArray() (第 A 行) 時省略型別參數,因為 TypeScript 可以從參數 elem 推斷出 T

// %inferred-type: string[]
const arr1 = fillArray<string>(3, '*');
assert.deepEqual(
  arr1, ['*', '*', '*']);

// %inferred-type: string[]
const arr2 = fillArray(3, '*'); // (A)

7.16 結論:了解初始範例

讓我們使用所學到的知識來了解我們先前看過的一段程式碼

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

這是元素型別為 T 的陣列介面