[TypeScript] 介面

型別別名與介面

型別別名與介面非常相似,使用物件型別別名來描述物件具有 born: number 和 name: string 的語法:

type Poet = {
  born: number;
  name: string;
};

然而,如果是使用介面,語法是:

interface Poet {
  born: number;
  name: string;
}

喜歡分號的開發者,通常將分號放在型別別名之後,而不是在介面之後。

然後就可以使用以下語法來設定 valueLater 的型別或介面為 Poet:

let valueLater: Poet;

然而,介面和型別別名之間,有幾個區別:

  • 介面可以「合併」,進行擴充。
  • 介面可用於類別宣告做型別結構的檢查,而型別別名不能。
  • 使用介面,可以讓 TypeScript 型別檢查變得更快速:在內部宣告一個可以更容易暫存的命名型別,而非像型別別名那樣,動態複製和貼上檢查新物件字面資料。
  • 因為介面被認為是命名物件,而不是未命名物件的別名,所以它們的錯誤訊息在一些情況下,更易閱讀。

屬性型別

可選擇的屬性

可透過問號,來表示介面的屬性是可選擇的:

interface Book {
  author?: string; // author 屬性是可選的
  pages: number;
}

// 正確
const ok: Book = {
  author: "abc",
  pages: 80
};

// 以下報錯:Property 'pages' is missing in type '{ author: string; }' but required in type 'Book'.
const missing: Book = {
  author: "def"
};

唯讀屬性

可以在屬性名稱之前,加上 readonly 關鍵字,來表示該屬性是唯讀的,不能重新指派新的資料。

例:

// Page 介面的 text 屬性,可存取然後會回傳字串,但如果指派新資料給 text 的話,會導致型別錯誤
interface Page {
  readonly text: string;
}

function read(page: Page) {
  console.log(page.text); // 正確

  // 報錯:因為 text 是唯讀屬性
  page.text += "!";
}

const myPage: Page = {
  text: "hi"
};

// 報錯:因為 text 是唯讀屬性
myPage.text += "!";

函式和方法

TypeScript 允許將介面成員,宣告為函式的兩個方式:

  • 方法(Method)語法:宣告介面的成員,作為物件成員呼叫的函式,例如 member(): void。
  • 屬性(Property)語法:宣告介面的成員,為獨立函式,例如 member: () => void。

例如以下:method 和 property 成員,都是可以不帶參數呼叫,並回傳 string 的函式:

interface HasBothFunctionTypes {
  property: () => string;
  method(): string;
}

const hasBoth: HasBothFunctionTypes = {
  property: () => "",
  method() {
    return "";
  }
};

hasBoth.property(); // 正確
hasBoth.method();   // 正確

也可以使用問號來表示可選擇(optional),例:

interface HasBothFunctionTypes {
  property?: () => string;
  method?(): string;
}

方法和屬性宣告大多可以互換使用,一個主要的差別是:

  • 方法不能宣告為 readonly,但屬性可以。

呼叫特徵

宣告介面和物件型別會具有呼叫特徵(call signatures)。呼叫特徵看起來類似於函式型別,但用冒號取代箭頭,例:

type FunctionAlias = (input: string) => number;

interface CallSignature {
  (input: string): number;
}

// 型別:(input: string) => number
const typedFunctionAlias: FunctionAlias = (input) => input.length; // 正確

// 型別:(input: string) => number
const typedCallSignature: CallSignature = (input) => input.length; // 正確

下例,keepsTrackOfCalls 函式宣告中,讓 count 屬性具有 number 型別,使其可指派給 FunctionWithCount 介面:

interface FunctionWithCount {
  count: number;
  (): void;
}

let hasCallCount: FunctionWithCount;

function keepsTrackOfCalls() {
  keepsTrackOfCalls.count += 1;
  console.log("已被執行" + keepsTrackOfCalls.count + "次。");
}

keepsTrackOfCalls.count = 0;

hasCallCount = keepsTrackOfCalls; // 正確

function doesNotHaveCount() {
  console.log("No idea!");
}

// 報錯:型別 () => void 缺少屬性 count,型別 FunctionWithCount 必須具有該屬性。
hasCallCount = doesNotHaveCount;

索引特徵

TypeScript 提供了一種稱為索引特徵(index signature)的語法,用來表示可以接受任何的字串 key,並回傳該 key 下的特定型別。最常用於字串鍵值。

例如以下寫法:

// WordCounts 是一個可以接受任何字串 key,然後都要回傳 number 型別的一個介面
interface WordCounts {
  [i: string]: number;
}

const counts: WordCounts = {};
counts.apple = 0;  // 正確
counts.banana = 1; // 正確

// 報錯:型別 boolean 不能指派給型別 number
counts.cherry = false;

索引特徵會有一個問題:無論存取什麼屬性,預設上,物件都會回傳一個資料。例:

interface DatesByName {
  [i: string]: Date;
}

const a: DatesByName = {
  b: new Date("1 January 1818")
};

a.b; // 型別 Date
console.log(a.b.toString()); // 正確

a.c; // 型別 Date,但執行時為 undefined
console.log(a.c.toString()); // 在型別系統中會是正確的,但執行時期會報錯:無法讀取屬性 toString

混合的屬性和索引特徵

介面能夠包含明確的屬性命名和包羅萬象的 string 索引特徵。有一點要留意:每個命名屬性的型別,必須可以指派給一般的索引特徵型別。所以可以混合,就像告訴 TypeScript 命名屬性制訂出更具體的型別,而任何其它屬性都可以回退到索引特徵的型別。例:

// HistoricalNovels 介面,宣告所有屬性都是 number 型別,但 Oroonoko 屬性必須存在
interface HistoricalNovels {
  Oroonoko: number;
  [i: string]: number;
}

// 正確
const novels: HistoricalNovels = {
  Outlander: 1991,
  Oroonoko: 1688
};

// 報錯,型別 {Outlander: number} 缺少屬性 Oroonoko。
const missingOroonoko: HistoricalNovels = {
  Outlander: 1991
};

再看以下例子:

// ChapterStarts 介面,宣告 preface 屬性必須為 0,而其它屬性為 number 型別。
interface ChapterStarts {
  preface: 0;
  [i: string]: number;
}

const a: ChapterStarts = {
  preface: 0,
  night: 1,
  shopping: 5
};

// 以下報錯:型別 1 不可指派給型別 0。
const b: ChapterStarts = {
  preface: 1
};

數字索引特徵

JavaScript 會預設將 key 轉換為字串。然而,也可以使用數值當作物件的 key。然而在 TypeScript 中,該如何撰寫呢:

// 允許,因為 string 可指派給 string | undefined。
interface MoreNarrowNumbers {
  [i: number]: string;
  [i: string]: string | undefined;
}

// 正確
const mixesNumbersAndStrings: MoreNarrowNumbers = {
  0: "",
  key1: "",
  key2: undefined
};

// 報錯:number 索引型別 string | undefined 無法指派給 string 索引型別 string。
interface abc {
  [i: number]: string | undefined;
  [i: string]: string;
}

巢狀介面

介面也可以寫成巢狀的形式。例:

interface Novel {
  author: {
    name: string;
  };
  setting: Setting;
}
interface Setting {
  place: string;
  year: number;
}

let myNovel: Novel;

// 正確
myNovel = {
  author: {
    name: "aaa"
  },
  setting: {
    place: "bbb",
    year: 1812
  }
};

let myNovel2: Novel;
myNovel2 = {
  author: {
    name: "aaa"
  },
  setting: {
    // 錯誤:型別 {place: string} 缺少屬性 year
    place: "bbb"
  }
};

介面繼承

一個介面,可以包含另一個介面的所有相同成員,並且還會加入一些額外的功能。

TypeScript 允許一個介面 繼承或稱做擴充(extend) 另一個介面,會複製另一個介面的所有成員。

語法及範例:

interface Writing {
  title: string;
}
// Novella 介面繼承 Writing 介面。
// Novella 稱做衍生介面;Writing 稱做基本介面。
interface Novella extends Writing {
  pages: number;
}

// 正確
let myNovella: Novella = {
  pages: 195,
  title: "aaa"
};

// 報錯:少了 pages 屬性
let abc: Novella = {
  title: "aaa"
};

// 報錯:只可以指定已知的屬性,所以不能有 author 屬性
let def: Novella = {
  pages: 195,
  title: "aaa",
  author: "John"
};

覆寫(override)的屬性

TypeScript 的型別檢查,將強制被覆寫的屬性,必須可以指派給它的基本屬性,這樣做是為了確保衍生介面型別的實作,可保持指派給基本介面型別。

例:

interface WithNullableName {
  name: string | null;
}

// 正確
interface WithNonNullableName extends WithNullableName {
  name: string;
}

// 會報錯:屬性 name 的型別不相容
interface WithNumericName extends WithNullableName {
  name: number | string;
}

擴充(繼承)多個介面

在衍生介面名稱之後的 extends 關鍵字,可以使用多個數量的介面名稱,用逗號分隔即可。

例:

interface a {
  giveA(): number;
}

interface b {
  giveB(): string;
}

interface c extends a, b {
  giveC(): number | string;
}

function my_func(instance: c) {
  instance.giveC(); // 型別 number | string
  instance.giveA(); // 型別 number
  instance.giveB(); // 型別 string
}

介面合併

如果兩個介面以相同的名稱宣告在同一個 scope 當中,它們將會自動合併。(建議少用此方式)

例:

interface Merged {
  fromFirst: string;
}

interface Merged {
  fromSecond: number;
}

// 以上等同於:
// interface Merged {
//   fromFirst: string;
//   fromSecond: number;
// }

成員命名上的衝突

例:

interface MergedProperties {
  same: (input: boolean) => string;
  different: (input: string) => string;
}

interface MergedProperties {
  same: (input: boolean) => string; // 正確
  // 以下會報錯,因為跟前面的衝到了。
  different: (input: number) => string;
}

合併的介面可以定義具有相同名稱和不同特徵的方法。這樣做會為該方法建立一個函式重載(overload)。

例:

interface MergedMethods {
  different(input: string): string;
}

interface MergedMethods {
  different(input: number): string; // 正確
}

留言

發佈留言