[TypeScript] 物件

物件型別

當使用 {…} 語法來建立物件字面型態時,TypeScript 將根據其屬性將其視為新的物件型別。

例如以下程式:

// 物件字面
const poet = {
  born: 1935,
  name: "Hi"
};

TypeScript 能分析出 poet 變數是具有兩個屬性的物件型別:born 是 number 型別、name 為 string 型別。

若嘗試存取任何其它屬性資料,會導致該名稱不在存的型別錯誤,例:

poet.end; // 報錯:型別 '{born: number; name: string;}' 沒有屬性 end。

物件型別是 TypeScript 如何解析 JavaScript 程式碼的核心概念。除了 null 和 undefined 之外的每個資料都有其支援型別,因此 TypeScript 必須理解每個變數的物件型別才能進行型別檢查。

宣告物件型別

明確宣告物件的型別,是個比較好的方式。使用物件型別類似於物件字面,但會使用型別而不是物件字面數值的語法來做描述。例:

// 明確的宣告物件型別
let poetLater: {
  born: number;
  name: string;
};

// 正確
poetLater = {
  born: 1935,
  name: "Hi"
};

// 錯誤:型別 string 不可指派給型別 '{ born: number; name: string; }'
poetLater = "Sappho";

物件型別別名

反覆寫出像 { born: number; name: string; } 會很快令人厭煩,所以可使用型別別名,來為每個型別形態指定一個名稱。例:

// 物件型別別名
type Poet = {
  born: number;
  name: string;
};

let poetLater: Poet;

// 正確
poetLater = {
  born: 1935,
  name: "Hi"
};

// 錯誤:型別 string 不可指派給型別 '{ born: number; name: string; }'
poetLater = "Hi";

大多數的 TypeScript 專案更偏好使用 interface 關鍵字來描述物件型別。物件型別別名和 interface 幾乎相同。

結構型別

TypeScript 是結構化型別(structuraly typed);JavaScript 是鴨子型別(duck typing)。

  • 結構型別適用於在靜態型別檢查時。
  • 鴨子型別通常是在執行時使用,在執行之前沒有檢查物件型別。

針對結構化型別:意味著只要滿足型別的資料,該資料都可以指派給變數。例:

type WithFirstName = {
  firstName: string;
};

type WithLastName = {
  lastName: string;
};

const hasBoth = {
  firstName: "Lucille",
  lastName: "Clifton"
};

// 允許,因為 withFirstName 的型別是 WithFirstName,只要 hasBoth 這個資料至少要有 firstName 即可。
let withFirstName: WithFirstName = hasBoth;

// 允許,因為 withLastName 的型別是 WithLastName,只要 hasBoth 這個資料至少要有 lastName 即可。
let withLastName: WithLastName = hasBoth;

型別檢查

再看一個例子:

type FirstAndLastNames = {
  first: string;
  last: string;
};

// 允許
const hasBoth: FirstAndLastNames = {
  first: "abc",
  last: "def"
};

// 報錯:型別 '{first: string; }' 缺少屬性 last。
const hasOnlyOne: FirstAndLastNames = {
  first: "hhh"
};

再看一個例子:

type TimeRange = {
  start: Date;
};

const hasStartString: TimeRange = {
  // 報錯:型別 string 不可指派給型別 Date。
  start: "1999-01-01"
};

多餘屬性的檢查

使用物件型別宣告變數,並且其初始值的欄位多於其型別描述的欄位,TypeScript 將會報錯。例:

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

// 允許
const poetMatch: Poet = {
  born: 1928,
  name: "Hi"
};

// 報錯:型別 '{ activity: string; born: number; name: string; }' 不可指派給型別 Poet。
const extraProperty: Poet = {
  activity: "Hi",
  born: 1935,
  name: "Hi2"
};

注意:這個檢查只會觸發在宣告建立物件型別的位置文字上。直接提供現有的物件,會饒過多餘的屬性檢查。

以下這個例子,extraPropertyButOk 變數不會觸發前一個例子當中的錯誤,因為它的初始值恰好在結構比對 Poet 的位置上:

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

const existingObject = {
  activity: "Hi",
  born: 1935,
  name: "Hi22"
};

// 允許
const extraPropertyButOk: Poet = existingObject;

多餘屬性的檢查,將在任何位置建立新物件時觸發,在該位置期望與物件型別一致。

巢狀物件型別

跟之前的概念是一樣的。例:

type Poem = {
  author: {
    firstName: string;
    lastName: string;
  };
  name: string;
};

// 允許
const poemMatch: Poem = {
  author: {
    firstName: "first",
    lastName: "last"
  },
  name: "lady"
};

const poemMismatch: Poem = {
  // 報錯:型別 '{ name: string; }' 不可指派給型別 '{ firstName: string; lastName: string; }'
  author: {
    name: "Hi"
  },
  name: "lady"
};

也可以改成這樣寫:

type Author = {
  firstName: string;
  lastName: string;
};

type Poem = {
  author: Author;
  name: string;
};

const poemMismatch: Poem = {
  // 報錯:型別 '{ name: string; }' 不可指派給型別 Author。
  author: {
    name: "Hi"
  },
  name: "lady"
};

像這樣將巢狀物件型別移到其它的型別名稱當中,是好的方法,可提高程式碼和錯誤訊息的可讀性。

可選擇的屬性

物件型別屬性,在某些物件中不見得都是必要的。所以我們可以在冒號之前加入問號(?),來表示它是一個可選擇的屬性。例:

type Book = {
  author?: string; // 表示 author 屬性是非必須的
  pages: number;
};

// 允許
const ok: Book = {
  author: "Hi",
  pages: 80
};

// 報錯:型別 '{ author: string; }' 缺少屬性 pages,型別 Book 必須有該屬性。
const missing: Book = {
  author: "Hi"
};

物件型別的聯集

推斷聯集的物件型別

如果一個變數可能是一個或多個物件型別之一作為作始值,TypeScript 將推斷它的型別,會是物件型別的聯集。

例如以下這段程式:

const poem = Math.random() > 0.5 ? { name: "Hi", pages: 7 } : { name: "hi2", rhymes: true };

判斷出來的型別如下圖紅框處:

明確的聯集物件型別

上個例子的推斷可能不是想要的。我們可以用另一種更明確也較好的方式:可以透過明確表示我們自己所使用的物件型別聯集來描述的物件型別。

type PoemWithPages = {
  name: string;
  pages: number;
};

type PoemWithRhymes = {
  name: string;
  rhymes: boolean;
};

type Poem = PoemWithPages | PoemWithRhymes;

const poem: Poem = Math.random() > 0.5 ? { name: "Hi", pages: 7 } : { name: "Hi2", rhymes: true };

poem.name; // 允許

// 會報錯。型別 Poem 沒有屬性 pages。型別 PoemWithRhymes 沒有屬性 pages。
poem.pages;

// 會報錯。型別 Poem 沒有屬性 rhymes。型別 PoemWithPages 沒有屬性 rhymes。
poem.rhymes;

接下來,必須進一步型別窄化才得以存取。

窄化物件型別

延續前一個例子,如果寫這樣:

type PoemWithPages = {
  name: string;
  pages: number;
};

type PoemWithRhymes = {
  name: string;
  rhymes: boolean;
};

type Poem = PoemWithPages | PoemWithRhymes;

if("pages" in poem){
  poem.pages; // 這是允許的,poem 被窄化成 PoemWithPages。
} else {
  poem.rhymes; // 這是允許的,poem 被窄化成 PoemWithRhymes。
}

可辨識的型別

JavaScript 和 TypeScript 中,聯集型別的另一種常見形式是在物件上,會用一個屬性來指示物件的型態。這種型別形態稱為可辨識的聯集(discriminant union),其值表示物件型別的屬性是可被分辨的。例:

type PoemWithPages = {
  name: string;
  pages: number;
  type: 'pages';
};

type PoemWithRhymes = {
  name: string;
  rhymes: boolean;
  type: 'rhymes';
};

type Poem = PoemWithPages | PoemWithRhymes;

const poem: Poem = Math.random() > 0.5 ? { name: "Hi", pages: 7, type: "pages"} : { name: "Hi2", rhymes: true, type: "rhymes" };

if(poem.type === "pages"){
  console.log(`可使用 ${poem.pages}`);
} else {
  console.log(`可使用 ${poem.rhymes}`);
}

poem.type; // 型別:'pages' | 'rhymes'

poem.pages; // 報錯:型別 Poem 沒有屬性 pages。型別 PoemWithRhymes 沒有屬性 pages。

交集型別

TypeScript 允許同時表示多個型別:使用 & 符號來表示交集型別。

例如以下例子:

type Artwork = {
  genre: string;
  name: string;
};

type Writing = {
  pages: number;
  name: string;
};

type WrittenArt = Artwork & Writing;
// 等同於:
// {
//   genre: string;
//   name: string;
//   pages: number;
// }

交集型別可以和聯集型別一起使用,這也有助於在一種型別中描述可辨識的聯集。例:

type ShortPoem = { author: string; } & ( | { kigo: string; type: "haiku"; } | { meter: number; type: "villanelle"; });

// 允許
const morningGlory: ShortPoem = {
  author: "Hi",
  kigo: "abc",
  type: "haiku"
};

// 這會報錯
const oneArt: ShortPoem = {
  author: "Hi",
  type: "villanelle"
};

never 型別

交集型別容易被誤用,並建立出一個不可能的型別。例如原始資料型別不能作為交集型別組成的一部份,因為一個變數不可能是多個原始資料型別。

例如以下例子,將導致 never 型別呈現出來:

type NotPossible = number & string;
// 型別 never

這個 never 關鍵字的型別是底限型別(bottom type)空型別。never 型別是一種不可能的資料,而且無法轉換的型別。沒有型別能代替 never 型別。

例:

type NotPossible = number & string;

// 報錯:型別 number 不可指派給型別 never。
let notNumber: NotPossible = 0;

// 報錯:型別 string 不可指派給型別 never。
let notString: never = "";

基本上,大多數專案不太會使用到 never 型別。

留言

發佈留言