Skip to main content

[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#

  • 使用在 arraow 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 支援「數值」和「字串」,也可以同時支援,但兩者只能要回傳相同型別的值:

interface StringArray {
[index: number]: string;
}
let stringArray: StringArray = ['Bob', 'Aaron'];

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

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

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

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

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

/* Index Signatures or Dictionary Type */
interface PhoneNumberDict {
// 因為物件的 key 可能沒有對應的 value,因此要記得加上 undefined
[numberName: string]: undefined | {
areaCode: number;
num: number;
}
// 搭配 indexed signatures 使用時,表示這兩個屬性必須存在
home: {
areaCode: number;
num: number;
},
office: {
areaCode: number;
num: number;
}
}
const phoneDict: PhoneNumberDict = {
// home: {araeCode: 321, num: 333}, // 可以避免 typo
home: {areaCode: 123, num: 456},
office: {areaCode: 321, num: 456},
// 因為有定義 indexed signatures,所以可以添加額外的屬性
registered: {areaCode: 666, num: 333},
}

重複名稱的 interface#

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

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.)

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

  • 根據「TypeScript 成為你全端開發的 ACE!」所述,雖然 Type 可以搭配聯集和交集來達到型別的擴展,但嚴格來說這算是定義出「新的靜態型別」,而不是「型別擴展」。

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);

參考資料#

Last updated on