- 對象成員語法形式
- 1)對象屬性
- 2)對象的屬性索引
- 3)對象的方法
- 4)函數
- 5)構造函數
- interface 的繼承
- interface 繼承 interface
- interface 繼承 type
- interface 繼承 class
- 接口合并
- interface 與 type 的異同
interface
是對象的模板,可以看作是一種類型約定,中文譯為“接口”。使用了某個模板的對象,就擁有了指定的類型結構。
interface Person {firstName: string;lastName: string;age: number;
}
上面示例中,定義了一個接口Person,它指定一個對象模板,擁有三個屬性firstName、lastName和age。任何實現這個接口的對象,都必須部署這三個屬性,并且必須符合規定的類型。
實現該接口很簡單,只要指定它作為對象的類型即可。
const p: Person = {firstName: "John",lastName: "Smith",age: 25,
};
上面示例中,變量p的類型就是接口Person,所以必須符合Person指定的結構。
方括號運算符可以取出 interface
某個屬性的類型。
interface Foo {a: string;
}type A = Foo["a"]; // string
上面示例中,Foo[‘a’] 返回屬性a的類型,所以類型A就是string。
對象成員語法形式
interface
可以表示對象的各種語法,它的成員有 5 種形式。
- 對象屬性
- 對象的屬性索引
- 對象方法
- 函數
- 構造函數
1)對象屬性
interface Point {x: number;y: number;
}
上面示例中,x和y都是對象的屬性,分別使用冒號指定每個屬性的類型。
屬性之間使用分號或逗號分隔,最后一個屬性結尾的分號或逗號可以省略。
如果屬性是可選的,就在屬性名后面加一個問號。
interface Foo {x?: string;
}
如果屬性是只讀的,需要加上 readonly
修飾符。
interface A {readonly a: string;
}
2)對象的屬性索引
interface A {[prop: string]: number;
}
上面示例中,[prop: string]
就是屬性的字符串索引,表示屬性名只要是字符串,都符合類型要求。
屬性索引共有string
、number
和symbol
三種類型。
一個接口中,最多只能定義一個字符串索引。字符串索引會約束該類型中所有名字為字符串的屬性。
interface MyObj {[prop: string]: number;a: boolean; // 編譯錯誤
}
上面示例中,屬性索引指定所有名稱為字符串的屬性,它們的屬性值必須是數值(number)。屬性a的值為布爾值就報錯了。
屬性的數值索引,其實是指定數組的類型。
interface A {[prop: number]: string;
}const obj: A = ["a", "b", "c"];
上面示例中,[prop: number]
表示屬性名的類型是數值,所以可以用數組對變量obj賦值。
同樣的,一個接口中最多只能定義一個數值索引。數值索引會約束所有名稱為數值的屬性。
如果一個 interface 同時定義了字符串索引和數值索引,那么數值索性必須服從于字符串索引。因為在 JavaScript 中,數值屬性名最終是自動轉換成字符串屬性名。
interface A {[prop: string]: number;[prop: number]: string; // 報錯
}interface B {[prop: string]: number;[prop: number]: number; // 正確
}
上面示例中,數值索引的屬性值類型與字符串索引不一致,就會報錯。數值索引必須兼容字符串索引的類型聲明。
3)對象的方法
對象的方法共有三種寫法。
// 寫法一
interface A {f(x: boolean): string;
}// 寫法二
interface B {f: (x: boolean) => string;
}// 寫法三
interface C {f: { (x: boolean): string };
}
屬性名可以采用表達式,所以下面的寫法也是可以的。
const f = "f";interface A {[f](x: boolean): string;
}
重載(Overloading)是指在同一個作用域中,允許多個同名的方法或函數根據參數的數量或類型不同而有不同的實現。在 TypeScript 的接口(interface
)中,方法可以聲明多種參數和返回值類型的組合,這就是方法重載。例如:
interface A {f(): number;f(x: boolean): boolean;f(x: string, y: string): string;
}
上面例子中,接口 A
的方法 f
有三種不同的參數簽名,這就是方法的重載。實現時,可以根據傳入參數的不同,執行不同的邏輯。
interface
里面的函數重載,不需要給出實現。但是,由于對象內部定義方法時,無法使用函數重載的語法,所以需要額外在對象外部給出函數方法的實現。
interface A {f(): number;f(x: boolean): boolean;f(x: string, y: string): string;
}function MyFunc(): number;
function MyFunc(x: boolean): boolean;
function MyFunc(x: string, y: string): string;
function MyFunc(x?: boolean | string, y?: string): number | boolean | string {if (x === undefined && y === undefined) return 1;if (typeof x === "boolean" && y === undefined) return true;if (typeof x === "string" && typeof y === "string") return "hello";throw new Error("wrong parameters");
}const a: A = {f: MyFunc,
};
上面示例中,接口A的方法f()有函數重載,需要額外定義一個函數MyFunc()實現這個重載,然后部署接口A的對象a的屬性f等于函數MyFunc()就可以了。
4)函數
interface
也可以用來聲明獨立的函數。
interface Add {(x: number, y: number): number;
}const myAdd: Add = (x, y) => x + y;
上面示例中,接口Add聲明了一個函數類型。
5)構造函數
interface
內部可以使用 new
關鍵字,表示構造函數。
interface ErrorConstructor {new (message?: string): Error;
}
上面示例中,接口 ErrorConstructor 內部有 new
命令,表示它是一個構造函數。TypeScript 里面,構造函數特指具有 constructor
屬性的類。
interface 的繼承
interface
可以繼承其他類型,主要有下面幾種情況。
interface 繼承 interface
interface
可以使用 extends
關鍵字,繼承其他 interface
。
interface Shape {name: string;
}interface Circle extends Shape {radius: number;
}
上面示例中,Circle 繼承了 Shape,所以Circle其實有兩個屬性name和radius。這時,Circle是子接口,Shape是父接口。
extends
關鍵字會從繼承的接口里面拷貝屬性類型,這樣就不必書寫重復的屬性。
interface
允許多重繼承。
interface Style {color: string;
}interface Shape {name: string;
}interface Circle extends Style, Shape {radius: number;
}
上面示例中,Circle 同時繼承了 Style 和 Shape,所以擁有三個屬性color、name和radius。
多重接口繼承,實際上相當于多個父接口的合并。
如果子接口與父接口存在同名屬性,那么子接口的屬性會覆蓋父接口的屬性。注意,子接口與父接口的同名屬性必須是類型兼容的,不能有沖突,否則會報錯。
interface Foo {id: string;
}interface Bar extends Foo {id: number; // 報錯
}
上面示例中,Bar繼承了Foo,但是兩者的同名屬性id的類型不兼容,導致報錯。
多重繼承時,如果多個父接口存在同名屬性,那么這些同名屬性不能有類型沖突,否則會報錯。
interface Foo {id: string;
}interface Bar {id: number;
}// 報錯
interface Baz extends Foo, Bar {type: string;
}
上面示例中,Baz同時繼承了Foo和Bar,但是后兩者的同名屬性id有類型沖突,導致報錯。
interface 繼承 type
interface
可以繼承 type
命令定義的對象類型。
type Country = {name: string;capital: string;
};interface CountryWithPop extends Country {population: number;
}
上面示例中,CountryWithPop 繼承了 type 命令定義的 Country 對象,并且新增了一個population 屬性。
注意,如果 type
命令定義的類型不是對象,interface
就無法繼承。
interface 繼承 class
interface
還可以繼承 class
,即繼承該類的所有成員。
class A {x: string = "";y(): boolean {return true;}
}interface B extends A {z: number;
}
上面示例中,B 繼承了 A,因此 B 就具有屬性x、y()和z。
實現B接口的對象就需要實現這些屬性。
const b: B = {x: "",y: function () {return true;},z: 123,
};
上面示例中,對象 b 就實現了接口 B,而接口 B 又繼承了類 A。
某些類擁有私有成員和保護成員,interface
可以繼承這樣的類,但是意義不大。
class A {private x: string = "";protected y: string = "";
}interface B extends A {z: number;
}// 報錯
const b: B = {/* ... */
};// 報錯
class C implements B {// ...
}
上面示例中,A 有私有成員和保護成員,B 繼承了 A,但無法用于對象,因為對象不能實現這些成員。這導致 B 只能用于其他 class
,而這時其他 class
與 A 之間不構成父類和子類的關系,使得 x 與 y 無法部署。
接口合并
多個同名接口會合并成一個接口。
interface Box {height: number;width: number;
}interface Box {length: number;
}
上面示例中,兩個Box接口會合并成一個接口,同時有height、width和length三個屬性。
這樣的設計主要是為了兼容 JavaScript 的行為。JavaScript 開發者常常對全局對象或者外部庫,添加自己的屬性和方法。那么,只要使用 interface
給出這些自定義屬性和方法的類型,就能自動跟原始的 interface
合并,使得擴展外部類型非常方便。
舉例來說,Web 網頁開發經常會對 windows
對象和 document
對象添加自定義屬性,但是 TypeScript 會報錯,因為原始定義沒有這些屬性。解決方法就是把自定義屬性寫成 interface
,合并進原始定義。
interface Document {foo: string;
}document.foo = "hello";
上面示例中,接口 Document 增加了一個自定義屬性 foo,從而就可以在 document
對象上使用自定義屬性。
同名接口合并時,同一個屬性如果有多個類型聲明,彼此不能有類型沖突。
interface A {a: number;
}interface A {a: string; // 報錯
}
上面示例中,接口 A 的屬性 a 有兩個類型聲明,彼此是沖突的,導致報錯。
同名接口合并時,如果同名方法有不同的類型聲明,那么會發生函數重載。而且,后面的定義比前面的定義具有更高的優先級。
interface Cloner {clone(animal: Animal): Animal;
}interface Cloner {clone(animal: Sheep): Sheep;
}interface Cloner {clone(animal: Dog): Dog;clone(animal: Cat): Cat;
}// 等同于
interface Cloner {clone(animal: Dog): Dog;clone(animal: Cat): Cat;clone(animal: Sheep): Sheep;clone(animal: Animal): Animal;
}
上面示例中,clone()方法有不同的類型聲明,會發生函數重載。這時,越靠后的定義,優先級越高,排在函數重載的越前面。比如,clone(animal: Animal)是最先出現的類型聲明,就排在函數重載的最后,屬于clone()函數最后匹配的類型。
這個規則有一個例外。同名方法之中,如果有一個參數是字面量類型,字面量類型有更高的優先級。
interface A {f(x: "foo"): boolean;
}interface A {f(x: any): void;
}// 等同于
interface A {f(x: "foo"): boolean;f(x: any): void;
}
上面示例中,f()方法有一個類型聲明參數x是字面量類型,這個類型聲明的優先級最高,會排在函數重載的最前面。
一個實際的例子是 Document 對象的createElement()方法,它會根據參數的不同,而生成不同的 HTML 節點對象。
interface Document {createElement(tagName: any): Element;
}
interface Document {createElement(tagName: "div"): HTMLDivElement;createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {createElement(tagName: string): HTMLElement;createElement(tagName: "canvas"): HTMLCanvasElement;
}// 等同于
interface Document {createElement(tagName: "canvas"): HTMLCanvasElement;createElement(tagName: "div"): HTMLDivElement;createElement(tagName: "span"): HTMLSpanElement;createElement(tagName: string): HTMLElement;createElement(tagName: any): Element;
}
上面示例中,createElement()方法的函數重載,參數為字面量的類型聲明會排到最前面,返回具體的 HTML 節點對象。類型越不具體的參數,排在越后面,返回通用的 HTML 節點對象。
如果兩個 interface
組成的聯合類型存在同名屬性,那么該屬性的類型也是聯合類型。
interface Circle {area: bigint;
}interface Rectangle {area: number;
}declare const s: Circle | Rectangle;s.area; // bigint | number
上面示例中,接口 Circle 和 Rectangle 組成一個聯合類型Circle | Rectangle。因此,這個聯合類型的同名屬性area,也是一個聯合類型。本例中的 declare
命令表示變量 s 的具體定義,由其他腳本文件給出。
interface 與 type 的異同
interface
命令與 type
命令作用類似,都可以表示對象類型。
很多對象類型即可以用 interface
表示,也可以用 type
表示。而且,兩者往往可以換用,幾乎所有的 interface
命令都可以改寫為 type
命令。
它們的相似之處,首先表現在都能為對象類型起名。
type Country = {name: string;capital: string;
};interface Coutry {name: string;capital: string;
}
上面示例是 type
命令和 interface
命令,分別定義同一個類型。
class
命令也有類似作用,通過定義一個類,同時定義一個對象類型。但是,它會創造一個值,編譯后依然存在。如果只是單純想要一個類型,應該使用 type
或 interface
。
interface
與 type
的區別有下面幾點。
(1)type
能夠表示非對象類型,而 interface
只能表示對象類型(包括數組、函數等)。
(2)interface
可以繼承其他類型,type
不支持繼承。
繼承的主要作用是添加屬性,type 定義的對象類型如果想要添加屬性,只能使用&
運算符,重新定義一個類型。
type Animal = {name: string;
};type Bear = Animal & {honey: boolean;
};
上面示例中,類型 Bear 在 Animal 的基礎上添加了一個屬性 honey。
上例的 &
運算符,表示同時具備兩個類型的特征,因此可以起到兩個對象類型合并的作用。
作為比較,interface
添加屬性,采用的是繼承的寫法。
interface Animal {name: string;
}interface Bear extends Animal {honey: boolean;
}
繼承時,type
和 interface
是可以換用的。interface
可以繼承 type
。
type Foo = { x: number };interface Bar extends Foo {y: number;
}
type 也可以繼承 interface。
interface Foo {x: number;
}type Bar = Foo & { y: number };
(3)同名 interface
會自動合并,同名 type
則會報錯。也就是說,TypeScript 不允許使用 type 多次定義同一個類型。
type A = { foo: number }; // 報錯
type A = { bar: number }; // 報錯
上面示例中,type兩次定義了類型A,導致兩行都會報錯。
作為比較,interface
則會自動合并。
interface A {foo: number;
}
interface A {bar: number;
}const obj: A = {foo: 1,bar: 1,
};
上面示例中,interface
把類型A的兩個定義合并在一起。
這表明,interface
是開放的,可以添加屬性,type
是封閉的,不能添加屬性,只能定義新的 type。
(4)interface
不能包含屬性映射(mapping),type
可以
interface Point {x: number;y: number;
}// 正確
type PointCopy1 = {[Key in keyof Point]: Point[Key];
};// 報錯
interface PointCopy2 {[Key in keyof Point]: Point[Key];
};
(5)this
關鍵字只能用于 interface
。
// 正確
interface Foo {add(num: number): this;
}// 報錯
type Foo = {add(num: number): this;
};
上面示例中,type 命令聲明的方法add(),返回this就報錯了。interface
命令沒有這個問題。
下面是返回 this 的實際對象的例子。
interface GetInfo {(x:number): this
}const getInfo:GetInfo = function (x: number) {return this;
}
(6)interface
可以直接擴展原始數據類型,type只能間接實現
-
interface
我們可以通過擴展全局接口(Global Interfaces)來為原始數據類型添加新的方法或屬性。這是因為 TypeScript 的原始類型(如
string
、number
、boolean
等)都有對應的全局接口定義。// 擴展Number接口 interface Number {isEven(): boolean;toPercentage(): string; }// 實現擴展的方法 Number.prototype.isEven = function() {return this % 2 === 0; };Number.prototype.toPercentage = function() {return `${this * 100}%`; };// 使用示例 const num1 = 10; console.log(num1.isEven()); // 輸出: trueconst num2 = 0.75; console.log(num2.toPercentage()); // 輸出: "75%"
注意事項:在模塊化項目中,需要使用
declare global
包裹接口擴展:declare global {interface Number {// 擴展方法定義} }
但需要避免過度擴展原始類型,以免造成代碼維護困難和潛在沖突。那有沒有更好的方式來規避這個問題,我們接著看。
-
type
在 TypeScript 中,無法直接用
type
擴充原生 Number 類型,這是由 TypeScript 的類型系統設計決定的:原生類型(如Number)的擴展只能通過interface
的聲明合并實現,而type
不支持聲明合并。不過,可以通過
type
創建一個 “增強版數字類型”,并關聯擴展方法,間接實現類似效果:// 1. 用type定義增強型數字類型(本質仍是number,但帶有擴展語義) type EnhancedNumber = number;// 2. 定義擴展方法的類型(約束方法參數和返回值) type NumberExtensions = {isEven: (num: EnhancedNumber) => boolean;toPercent: (num: EnhancedNumber) => string; };// 3. 實現擴展方法 const enhance: NumberExtensions = {isEven: (num) => num % 2 === 0,toPercent: (num) => `${(num * 100)}%` };// 4. 使用示例 const num: EnhancedNumber = 0.25;// 調用擴展方法(通過工具對象關聯) console.log(enhance.isEven(4)); // 輸出: true console.log(enhance.toPercent(num)); // 輸出: "25.0%"// 仍可使用原生Number方法 console.log(num.toFixed(2)); // 輸出: "0.25"
核心區別說明:
- type的局限性:type 是類型別名,無法像 interface 那樣通過聲明合并擴展原生類型的方法集,只能通過工具函數 / 對象間接關聯擴展功能。
- 實現思路:通過
type EnhancedNumber = number
創建語義化類型,再用type約束擴展方法的類型,最后通過工具對象實現功能。 - 類型安全:這種方式仍能保證類型一致性,同時保留原生數字的所有特性。
如果需要直接在數字實例上調用擴展方法(如
num.isEven()
),必須使用 interface 的聲明合并,這是 TypeScript 規范中擴展原生類型的唯一方式。type 更適合創建語義化類型或組合類型,而非直接擴展原生類型的方法。
(7)interface
無法表達某些復雜類型(比如交叉類型和聯合類型),但是 type
可以。
type A = {/* ... */
};
type B = {/* ... */
};type AorB = A | B;
type AorBwithName = AorB & {name: string;
};
上面示例中,類型 AorB 是一個聯合類型,AorBwithName則是為AorB添加一個屬性。這兩種運算,interface 都沒法表達。
綜上所述,如果有復雜的類型運算,那么沒有其他選擇只能使用 type;一般情況下,interface靈活性比較高,便于擴充類型或自動合并,建議優先使用。