[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 alias
或interface
都存在。
// 將 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;
}
💡 當我們使用 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);
參考資料
- Interfaces @ TypeScript
- Day 12. 機動藍圖・介面宣告 X 使用介面 - TypeScript Interface Intro @ iT 邦幫忙鐵人賽
- Day 13. 機動藍圖・介面的延展 X 功能與意義 - Interface Extension & Significance @ iT 邦幫忙鐵人賽:說明 Type 和 Interface 的差別
- Day 14. 機動藍圖・函式超載 X 究極融合 - Function Overload & Interface Merging @ iT 邦幫忙鐵人賽