跳至主要内容

[TS] Interfaces

Interfaces @ TypeScript

定義 Interface

Interface 可以用來定義物件,還有由物件所延伸的型別(例如,陣列、函式)

Object

interface Person {
name: string;
age: number;
}

Object / Array

元素都是字串的陣列:

interface StringArray {
[index: number]: string;
}

任何屬性值都可以接收的物件:

interface Dictionary {
[propName: string]: any;
}

Object with methods 物件中的函式/方法

interface Bird {
fly(): void;
}

Function:單純定義函式的 Interface

  • 使用在 arrow function 或 function expression
interface GreetFunc {
(name: string, age: number): void;
}

// 如果用 type alias 來定義型別會長這樣
type GreetFunc2 = (name: string, age: number) => void;

// 可以不用額外指定參數型別(參數名稱可以不同)
let greet: GreetFunc = (n, a) => {
console.log(`Hello ${n}, you are ${a} years old`);
};

// 這個 greet function 會自動符合 Greet 這個 interface
let greet: GreetFunc = (name: string, age: number) => {
console.log(`Hello ${name}, you are ${age} years old`);
};

// 即使參數名稱不符合,只要型別符合即會算在這個 Interface
// 例如這裡改用 n, a 當作參數名稱,一樣符合 Greet Interface
let greet: GreetFunc = (n: string, a: number) => {
console.log(`Hello ${n}, you are ${a} years old`);
};

函式參數也可以使用物件的解構賦值:

interface GreetFunc {
(person: { name: string; age: number }): void;
}

const greet: GreetFunc = ({ name, age }) => {
console.log(`Hello ${name}, you are ${age} years old.`);
};

// 或者等同於
const greet = ({ name, age }: { name: string; age: number }) => {
console.log(`Hello ${name}, you are ${age} years old.`);
};

greet({
name: 'Aaron',
age: 32,
});

另外,當一個函式最終不一定有回傳值時,預設會是回傳 undefined,例如:

interface Foo {
(a: number): number;
}

// 由於 foo 這個函式最終可能沒有回傳值
// ❌ 修改 interface:因此在宣告 Foo 時,回傳值要是 `number | undefined`
const foo: Foo = (a) => {
if (a > 5) {
return 3;
}
};

// ⭕️ 修改函式:或者在函式最後拋出錯誤(never),這時候因為 `number | never` 等同於 `number`
// 因此將符合 interface 的定義
const bar: Foo = (a) => {
if (a > 5) {
return 3;
}
throw new Error('a is not over 5');
};

Hybrid:一個物件同時可以當作物件也可以當作函式

interface Shape {
color: string;
}

interface Square extends Shape {
(width: number): string;
width: number;
area(): void;
}

const getSquare = (): Square => {
let square = function (width: number) {
square.width = width;
} as Square;

square.area = () => {
console.log(`the square area is ${square.width * square.width}`);
};
return square;
};

// s 同時是物件,也是函式
const s = getSquare();
s(10);
s.area();
console.log(s.width);

將 Interface 當成 Type:不能多不能少

interface Person {
name: string;
age: number;
}

// ❌ interface 缺少 key 不行
const martin: Person = {
name: 'Martin',
};

// ❌ interface 多 key 不行
const leo: Person = {
name: 'Martin',
age: 30,
job: 'DevOps',
};

作為 Object Literal 的額外檢查(excess property checking):可以多不能少

當使用 Object Literal 是「直接帶入函式參數」或「指派給其他變數」時,會經過經過額外的檢查(excess property checking),需要與 Interface 完全符合才可以:

💡 excess property checking 的情況,不論是使用 type aliasinterface 都存在。

// 將 Interface 當成函式參數
function greet(person: Person) {
console.log(`${person.name} is ${person.age} years old`);
}

// ⭕️ 帶入 function 中的參數只要符合 Interface 即可,可以有多餘的屬性(例如,Job)
const aaron = {
name: 'Aaron',
age: 32,
job: 'Web Developer',
};
greet(aaron);

// ❌ 但如果沒有先宣告成變數會顯示錯誤,這種情況 TS 會直接把參數和 Person 比對,類似 arron: Person
greet({
name: 'Aaron',
age: 32,
job: 'Web Developer',
});

// ❌ 可以多不能少
const john = {
name: 'John',
};
greet(john);

方法一:使用 type assertion 來避免 excess property checking

如果要避免 object literal 直接帶入函式參數時的 excess property checking 的話,可以使用 type assertion 來解決:

greet({
name: 'Aaron',
age: 32,
job: 'Web Developer',
} as Person);

方法二:使用 index signature 來避免 excess property checking

interface Person {
// name 和 age 一定要存在
name: string;
age: string;
// 但可以接受額外的屬性
[propName: string]: string;
}

方法三:將物件指派成一個變數來避免 excess property checking

這個做法如同一開始的範例:

const aaron = {
name: 'Aaron',
age: 32,
job: 'Web Developer',
};
greet(aaron);

使用 Interface 當做套件的 options 物件

// https://www.staging-typescript.org/docs/handbook/interfaces.html
interface SquareConfig {
color?: string;
width?: number;
}

// 接受 SquareConfig 做為參數,回傳 {color: string; area: number}
const createSquare = (config: SquareConfig): { color: string; area: number } => {
let newSquare = { color: 'white', area: 100 };

if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}

return newSquare;
};

const mySquare = createSquare({ color: 'black' });
console.log(mySquare);

readonly

定義常數時使用 const,如果是希望某個物件的屬性是常數的話,則使用 readonly

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

const point: Point = {
x: 10,
y: 10,
};

point.x = 15; // ❌ x 是 readonly 不能被修改
point.y = 20; // ⭕️

Indexable Types / indexed signatures / dictionary

只需要使用 [] 就可以指定 indexable type。index signatures 支援「數值」和「字串」,也可以同時支援,但兩者只能回傳相同型別的值。可以透過 index signatures 來定義物件和陣列:

interface StringArray {
[index: number]: string;
}

let stringArray: StringArray = ['Bob', 'Aaron'];

少數時候會這樣用,這個物件/陣列的屬性值將會是 any:

interface Dictionary {
[propName: string]: any;
}

// 也可以搭配泛型使用
type Dict<T> = {
[K: string]: T;
};

使用 indexed signatures 來定義 object 中的 property 時:

interface PhoneNumber {
areaCode: number;
num: number;
}

/* Index Signatures or Dictionary Type */
interface PhoneNumberDict {
// 因為物件的 key 可能沒有對應的 value,因此要記得加上 undefined
[numberName: string]: undefined | PhoneNumber;
}

物件搭配 indexed signatures 可以說明哪些欄位是必要的

interface PhoneNumber {
areaCode: number;
num: number;
}

/* Index Signatures or Dictionary Type */
interface PhoneNumberDict {
// 因為物件的 key 可能沒有對應的 value,因此要記得加上 undefined
[numberName: string]: undefined | PhoneNumber;

// 搭配 indexed signatures 使用時,表示這兩個屬性必須存在
home: PhoneNumber;
office: PhoneNumber;
}

const phoneDict: PhoneNumberDict = {
// 一定要包含 home 和 Office 這兩個 property
home: { areaCode: 123, num: 456 },
// home: {araeCode: 321, num: 333}, // 可以避免 typo
office: { areaCode: 321, num: 456 },

// 因為有定義 indexed signatures,所以可以添加額外的屬性
registered: { areaCode: 666, num: 333 },
};

重複名稱的 interface(duplicate name)

interface 是 "open" 的,當有同樣名稱的 interface 被定義時,它們會被 merge。

舉例來說,當我們定義了三個相同名稱的 interface:

interface Product {
movie: string;
}

interface Product {
dessert: string;
}

interface Product {
book: string;
}

實際上這三個 interface 中的屬性會被合併在一起:

// 等同於
interface Product {
movie: string;
dessert: string;
book: string;
}
warning

💡 當我們使用 Type 搭配交集(&)使用時,當有重複屬性名稱,但對應型別有衝突時,該屬性的型別會變成 never

Extending Interfaces

interface Shape {
color: string;
}

interface Square extends Shape {
width: number;
}

// Triangle 這個 interface 是延伸自 Shape 和 Square
// 中文指的是:想要實作 Triangle 介面時,除了必須符合 Triangle 自身的屬性與方法外,
// 還必須實作出 Shape 和 Square 的屬性與方法
interface Triangle extends Shape, Square {
height: number;
}

const shape: Shape = {
color: 'red',
};

const square: Square = {
color: 'red',
width: 20,
};

const triangle: Triangle = {
color: 'red',
width: 20,
height: 30,
};
  • 不同介面之間可以有相同屬性的名稱,但其各自對應的型別不能衝突,否則 TypeScript 會報錯( Named property 'width' of types 'Foo' and 'Bar' are not identical.)

Function Overload(函式超載)

函式超載指的是擴充一個函式可以被執行的形式(例如,介面)。舉例來說,在實作函式(function implementation)時,提供了一個比較廣泛的型別。舉例來說,根據 getArea 帶入的參數型別不同,會自動對應到使用 GetArea Interface 中不同的型別:

// STEP 1:定義一個可以同時接收多個不同型態的函式
interface GetArea {
(p1: number, p2: number): number;
(p1: string, p2: string): number;
}

// STEP 2:在該函式中進行條件判斷,以符合 Interface 的定義
const getArea: GetArea = (p1: number | string, p2: number | string) => {
if (typeof p1 === 'number' && typeof p2 === 'number') {
return p1 + p2;
} else if (typeof p1 === 'string' && typeof p2 === 'string') {
return Number(p1) + Number(p2);
}

// 若不拋出錯誤的話,這裡會自動回傳 undefined 而非 never,將會不符合 interface 的定義
throw new Error('Invalid arguments');
};

const area = getArea(30, 30);
console.log(area);

參考資料