4. 接口的用法
约 4338 字大约 14 分钟
2025-05-14
TypeScript 的接口(interface
)是类型系统的核心概念之一,中文译为“接口”,主要用于定义对象的成员属性和方法。
interface
是对象的模板,是一种类型,接口只需要给出具体的结构定义即可,不需要给出具体的实现:
- 对象可以依据此类型创建指定的结构的数据。
- 类可以实现此接口,实现接口的所有成员。
- 接口本身不可以被
new
创建实例。
一、简介
1、接口的定义
接口最基本的功能是描述对象应该具有哪些属性和方法。
interface User {
id: number;
name: string;
email: string;
age?: number; // 可选属性
readonly createdAt: Date; // 只读属性
speak(): string; // 方法
}
成员之间使用分号或逗号分隔,也可以省略,使用换行代替。
如果成员是可选的,就在成员后面加一个问号 ?
。
如果成员是只读的,需要加上readonly
修饰符。
2、严格类型检查
接口定义了对象的结构,任何以此结构创建的对象都必须满足接口的类型检查。
成员的名称、类型和数量必须严格和定义保持一致。
interface Person {
firstName: string;
lastName: string;
age: number;
speak(): string;
}
const person: Person = {
firstName: "John",
lastName: "Doe",
age: 30,
speak() {
return `Hello, my name is ${this.firstName} ${this.lastName}.`;
},
};
3、接口的成员
interface
可以表示对象的各种语法,它的成员有 5 种形式:
1、对象属性
冒号两侧分别为属性的属性名和类型。
interface Point {
id: number;
name: string;
age?: number; // 可选属性
readonly createdAt: Date; // 只读属性
}
2、对象的属性索引
其他更详细的用法可以参考属性索引。
在 TypeScript 中,对象的属性索引(property index
)是一种特殊的属性,用于描述对象中属性的类型。
interface A {
[prop: prop_type]: value_type;
}
prop
:属性名,可以是任意字符串。prop_type
:属性名的类型,可以是string
、number
和symbol
,主要用于描述对象或者数组。value_type
:属性值的类型,可以是任何有效的 TypeScript 类型。
3、方法
方法共有三种写法。
interface User {
// 基础写法
speak(name: boolean): string;
// 类似箭头函数
walk: (name: boolean) => string;
// 对象作为函数类型
run: { (name: boolean): string };
}
//
属性名可以采用表达式,所以下面的写法也是可以的。
const func_name = "isAdult";
interface User {
[func_name](age: number): boolean;
// 等价于 ["isAdult"](age: number): boolean;
// 等价于 isAdult(age: number): boolean;
}
4、构造函数
interface 内部可以使用new
关键字,表示构造函数。
- 可以用作构造函数的模板,用来创建构造函数。
- 接口中的构造函数声明无法直接被实现
封装成为构造函数的原型,离不开两样东西:实现构造函数的函数(作为参数调用)、和构造函数签名一致的对象类型
interface Vehicle {
drive(): void;
}
interface VehicleConstructor {
new (model: string): Vehicle;
}
class Car implements Vehicle {
constructor(private model: string) {}
drive() {
console.log(`Driving ${this.model} car`);
}
}
class Truck implements Vehicle {
constructor(private model: string) {}
drive() {
console.log(`Hauling with ${this.model} truck`);
}
}
// 凡是作为构造函数的类型,必须作为函数的参数调用
function createVehicle(ctor: VehicleConstructor, model: string): Vehicle {
return new ctor(model);
}
const myCar = createVehicle(Car, "Tesla");
const myTruck = createVehicle(Truck, "Ford F-150");
myCar.drive();
myTruck.drive();
5、函数
interface 也可以用来声明独立的函数。一般用作函数对象的模板,用来创建函数。
下列代码中,声明了一个函数add
,它接受两个数字类型的参数x
和y
,并返回一个数字:
interface Add {
(x: number, y: number): number;
}
const myAdd: Add = (x, y) => x + y;
4、只读属性
在接口中,使用readonly
关键字来定义只读属性。只读属性表示该属性只能在对象创建时被赋值,之后不能被修改。
interface User {
readonly id: number;
name: string;
}
const user: User = { id: 1, name: "John" };
user.id = 2; // 错误:无法分配到 "id" ,因为它是只读属性。Cannot assign to 'id' because it is a read-only property.
5、可选属性
在接口中,使用?
关键字来定义可选属性。可选属性表示该属性可以存在,也可以不存在。
interface User {
id: number;
name: string;
age?: number; // 可选属性
}
const user1: User = { id: 1, name: "John" };
const user2: User = { id: 2, name: "Jane", age: 30 };
6、接口的方法重载
interface
里面的函数重载,不需要给出实现。但是,由于对象内部定义方法时,无法使用函数重载的语法,所以需要额外在对象外部给出函数方法的实现。
interface UserOverload {
speak(): number;
speak(x: boolean): boolean;
speak(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 xiaohong: UserOverload = {
speak: MyFunc,
};
xiaohong.speak();
xiaohong.speak(true);
xiaohong.speak("1", "2");
上面示例中,接口User
的方法speak()
有函数重载,需要额外定义一个函数MyFunc()
实现这个重载,然后部署接口User
的对象xioahong
的属性fspeak
等于函数MyFunc()
就可以了。
也可以使用一个范围更广的函数实现,在函数内部使用if
判断,来实现函数重载。
const xiaobai: UserOverload = {
speak: function (x?: boolean | string, y?: string): any {
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");
},
};
7、同名接口合并
同名的接口会自动合并,二者的属性和方法会被合并到一起,这在扩展第三方类型定义时特别有用。
如果涉及到同名成员则需要注意以下几点:
- 名字相同的属性,会严格检查类型是否相同,如果不同,会报错。
- 名称相同的方法,会进行方法重载。
interface Square {
title: string;
hight: number;
area(): number;
}
interface Square {
title: string;
width: number;
}
// 合并后的接口
const square: Square = {
title: "My App",
width: 1024,
hight: 768,
area() {
return this.width * this.hight;
},
};
以下是个错误示例:
interface Man {
age: string;
speak(): void;
}
interface Man {
age: Number;Subsequent property declarations must have the same type. Property 'age' must be of type 'string', but here has type 'Number'. speak(val: string): void;
}
8、成员混合使用
1、一般场景
接口可以定义对象的属性和方法,也可以定义数组的元素类型,也可以定义函数的参数和返回值。
// 测试接口的属性和方法
interface Person {
name: string;
age: number;
address?: string;
speak(): void;
}
const user: Person = {
name: "Alice",
age: 30, // 正确
address: "123 Main St", // 可选属性
speak() {
console.log("Hello, I am " + this.name);
},
};
user.speak();
console.log(user);
2、属性索引 + 固定属性
属性索引可以结合固定属性一起使用,用于描述含有特定属性的对象。
但是我们需要注意一下几点:
- 我们建议固定属性放在属性索引的前面,方便代码阅读。
- 固定属性的类型必须被属性索引的类型兼容。
- 接口中同时定义了字符串索引和数值索引的时候,数值索引必须服从于字符串索引,也可以称为父类型。
interface A {
name: string;
age?: number;Property 'age' of type 'number | undefined' is not assignable to 'string' index type 'string'. [key: string]: string;
}
interface B {
[prop: string]: number;
[prop: number]: string; // 报错'number' index type 'string' is not assignable to 'string' index type 'number'.}
interface C {
[prop: string]: any;
[prop: number]: number; // 正确
}
此处数字索引的类型范围 <= 字符串索引的类型范围
2、特定属性 + 函数
接口可以用来定义函数的类型,包括函数的参数和返回值。同时函数也是一种特殊的对象,可以包含一些属性:
interface Person {
name: string;
age: number;
}
interface GetName {
prop: string;
version: string;
(val: Person): string;
}
let user: Person = {
name: "zhangsan",
age: 18,
};
// const getUserName: GetName = function (user) {
// return user.name;
// }
const getUserName: GetName = (user) => {
return user.name;
};
// 必须同时给 prop 和 version 赋值,否则 ts 会报错
getUserName.prop = "任何有意义的语句";
getUserName.version = "v1.0.0";
getUserName(user);
上述示例中,我们创建了一个带属性属性的方法。
3、属性 + 方法 + 构造函数
接口中可以定义构造函数,同时也可以包含一些属性成员。但是这样的结构一般用于类的实现,没有单独使用的必要。
以下是模拟一个数据库连接的示例:
// 实例接口
interface DatabaseInstance {
query(sql: string): Promise<any[]>;
close(): void;
}
// 静态部分接口(包含构造函数和其他静态成员)
interface DatabaseConstructor {
new (connectionString: string): DatabaseInstance;
readonly version: string;
getSupportedDrivers(): string[];
}
// 实现
const Database: DatabaseConstructor = class Database
implements DatabaseInstance
{
static version = "2.3.1";
static getSupportedDrivers() {
return ["postgres", "mysql", "sqlite"];
}
constructor(private connectionString: string) {
// 初始化连接
}
async query(sql: string) {
console.log(`Executing: ${sql}`);
return [
/* 结果 */
];
}
close() {
console.log("Connection closed");
}
};
// 使用
const db = new Database("postgres://user:pass@localhost:5432/db");
console.log(Database.version); // "2.3.1"
db.query("SELECT * FROM users").then((res) => {
console.log(res); // 执行查询
db.close(); // 关闭连接
});
二、接口的继承
接口可以使用extends
关键字,继承其他类型的全部成员属性,从继承的类型里面拷贝属性类型,这样就不必书写重复的属性。
接口只能继承类对象结构的类型,如class
、interface
、type
定义的对象。
1、继承接口 interface
interface
可以继承其他interface
,从而获得其他interface
的所有成员属性。
下面示例中,Circle
继承了Shape
,所以Circle
其实有两个属性name
和radius
。这时,Circle
是子接口,Shape
是父接口。
interface Shape {
name: string;
}
interface Circle extends Shape {
radius: number;
}
// 等价于
interface Circle {
name: string;
radius: number;
}
上面示例中,CountryWithPop
继承了type
命令定义的Country
对象,并且新增了一个population
属性。
2、继承 type
interface
可以继承type
命令定义的对象类型。
注意,如果type
命令定义的类型不是对象,interface
就无法继承。
type Country = {
name: string;
capital: string;
};
interface CountryWithPop extends Country {
population: number;
}
3、继承类 class
类可以被接口继承,即接口可以继承类的成员、方法、属性。
下边的示例中,接口 B
继承了类 A
,因此接口 B
就具有属性 x
、y()
和 z
,B
接口的实例化对象 b
就需要实现这些属性。
class A {
x: string = "";
y(): boolean {
return true;
}
}
interface B extends A {
z: number;
}
const b: B = {
x: "",
y: function () {
return true;
},
z: 123,
};
interface
还可以继承 class
,即继承该类的所有的成员的定义。但是实际开发中,我们需要限制 接口只能继承类的公开成员。
接口可以继承类的私有成员和保护成员,但是下边的例子告诉我们:无论实现类是否实现了接口继承来的私有成员和保护成员,都会报错。
class A {
constructor(protected a: string, private b: string) {}
}
interface B extends A {
c: string;
}
class C implements B {Class 'C' incorrectly implements interface 'B'.
Type 'C' is missing the following properties from type 'B': a, b // Error:c 缺少 a 和 b 的实现
constructor(public c: string) {}
}
class D implements B {Class 'D' incorrectly implements interface 'B'.
Property 'a' is protected but type 'D' is not a class derived from 'A'. // Error:D 不是 A 的 子类
constructor(public c: string, protected a: string, private b: string) {}
}
4、多重继承
interface
允许多重继承,可以同时继承多个父目标。继承目标可以是接口、类或者对象三者的组合。
interface Style {
color: string;
}
type Shape = {
name: string;
};
class Size {
width: number;
height: number;
}
interface Circle extends Style, Shape, Size {
radius: number;
}
上面示例中,Circle
同时继承了Style
和Shape
,所以拥有三个属性color
、name
和radius
。
5、同名属性继承问题
接口在继承的时候,不管继承目标是接口、类还是对象,都会遇到一个相同的问题:同名属性继承。
本章节以接口为例,其他类型的继承行为类似。
1、接口和父接口之间
如果接口和父接口之间存在同名属性,则类型必须兼容(存在继承关系)。即子接口的同名属性类型必须是父接口的同名属性类型的子类型。
例如下面的代码中:Bar
继承了Foo
,但是两者的同名属性id
的类型不兼容,导致报错。
interface Foo {
id: string;
}
interface Bar extends Foo {Interface 'Bar' incorrectly extends interface 'Foo'.
Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'string'. id: number; // 报错
}
2、多个父接口之间
接口在多重继承时,父接口之间 如果有同名属性,则 类型必须相同。
例如下面代码中:Baz
同时继承了Foo
和Bar
,但是后两者的同名属性id
有类型冲突,导致报错。
interface Foo {
id: string;
}
interface Bar {
id: number;
}
// 报错
interface Baz extends Foo, Bar {Interface 'Baz' cannot simultaneously extend types 'Foo' and 'Bar'.
Named property 'id' of types 'Foo' and 'Bar' are not identical. type: string;
}
6、同名方法继承问题
继承时,同名方法的问题会比较复杂,因为方法的签名(参数类型和返回值类型)也会被继承。
注意:如果你的子接口的方法的能够与父接口的方法兼容,只需要在子接口中定义该方法即可完成继承。
1、基本继承行为
接口在继承的时候,还可能会遇到同名方法的问题。此时,子接口可以:
- 保持父接口的方法签名不变
- 重载方法(添加新的签名)
- 重写方法(覆盖签名)
interface Parent {
greet(name: string): string;
}
interface Child extends Parent {
// 保持相同签名
greet(name: string): string;
// 添加重载
greet(name: string, age: number): string;
// 或者完全覆盖(不推荐,可能会导致类型不兼容)
greet(name: number): string;
}
class Nino implements Child {
greet(name: any): string {
return "Hello, " + name;
}
}
const child = new Nino();
console.log(child.greet("John")); // 输出: Hello, John
console.log(child.greet(30)); // 输出: Hello, 30
2、方法合并规则
当多个接口被合并时(如多重继承),同名方法会被合并,:
interface A {
log(msg: string): void;
}
interface B {
log(msg: string, level: number): void;
}
interface C extends A, B {
log(msg: string): void;
log(msg: string, level: number): void;
// 合并后的log方法有两个重载签名
}
// 实现
class Logger implements C {
log(msg: string): void;
log(msg: string, level?: number): void {
console.log(level ? `[${level}] ${msg}` : msg);
}
}
3、签名兼容性要求
子接口的方法必须与父接口兼容:
interface Base {
process(data: string): number;
}
// 合法 - 参数更宽松(父接口要求string,子接口接受更宽泛的any)
interface ValidExtension extends Base {
process(data: any): number;
}
// 非法 - 参数更严格(父接口允许string,子接口要求更严格的"literal")
// interface InvalidExtension extends Base {
// process(data: "literal"): number; // 错误
// }
4、返回类型规则
interface Reader {
read(): { data: any };
}
// 合法 - 返回更具体的子类型
interface EnhancedReader extends Reader {
read(): { data: any; timestamp: number };
}
// 非法 - 返回更宽泛的类型
// interface InvalidReader extends Reader {
// read(): any; // 错误
// }
5、可选与必需属性的交互
interface Options {
debug?: boolean;
}
// 子接口可以使可选属性变为必需
interface StrictOptions extends Options {
debug: boolean; // 现在是必需的
}
// 但不能反过来使必需属性变为可选
interface BaseRequired {
debug: boolean;
}
// interface InvalidOptions extends BaseRequired {
// debug?: boolean; // 错误
// }
6、只读性规则
只读属性不影响同名方法的合并
interface ReadOnlyOptions {
readonly debug: boolean;
}
interface WritableOptions extends ReadOnlyOptions {
debug: boolean; // 现在是可写的
}
let a: WritableOptions = {
debug: true,
};
a.debug = false; // 现在是可写的
三、被 Class 类实现(面向对象)
接口可以定义类必须实现的属性和方法。
但是类只能实现对象类型的接口,无法直接实现函数类型和构造函数类型的接口。
interface ClockInterface {
currentTime: Date;
[prop: string]: any;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
}
四、具体使用场景
1、构建一个数据库原型
// 实例接口
interface DatabaseInstance {
query(sql: string): Promise<any[]>;
close(): void;
}
// 静态部分接口(包含构造函数和其他静态成员)
interface DatabaseConstructor {
new (connectionString: string): DatabaseInstance;
readonly version: string;
getSupportedDrivers(): string[];
}
// 实现
const Database: DatabaseConstructor = class Database
implements DatabaseInstance
{
static version = "2.3.1";
static getSupportedDrivers() {
return ["postgres", "mysql", "sqlite"];
}
constructor(private connectionString: string) {
// 初始化连接
console.log(`正在连接数据库 => ${connectionString}`);
}
async query(sql: string) {
console.log(`正在执行 SQL => ${sql}`);
return [
/* 结果 */
];
}
close() {
console.log("Connection closed");
}
};
// 使用
const db = new Database("postgres://user:pass@localhost:5432/db");
console.log("当前版本号:", Database.version); // "2.3.1"
db.query("SELECT * FROM users").then((res) => {
console.log(res); // 执行查询
db.close(); // 关闭连接
});
输出;
正在连接数据库 => postgres://user:pass@localhost:5432/db
当前版本号: 2.3.1
正在执行 SQL => SELECT * FROM users
[]
Connection closed
2、带默认配置的组件系统
// 实例接口
interface UIComponent {
render(): HTMLElement;
update(config: Partial<UIComponentConfig>): void;
}
// 静态接口(包含构造函数和默认配置)
interface UIComponentConstructor {
new (element: HTMLElement): UIComponent;
readonly defaultConfig: UIComponentConfig;
registerPlugin(plugin: Plugin): void;
}
// 配置类型
interface UIComponentConfig {
theme: "light" | "dark";
animations: boolean;
}
// 实现
const Button: UIComponentConstructor = class Button implements UIComponent {
static defaultConfig: UIComponentConfig = {
theme: "light",
animations: true,
};
static plugins: Plugin[] = [];
static registerPlugin(plugin: Plugin) {
this.plugins.push(plugin);
}
constructor(private element: HTMLElement) {}
render() {
const btn = document.createElement("button");
// 渲染逻辑
return btn;
}
update(config: Partial<UIComponentConfig>) {
// 更新逻辑
}
};
// 使用
console.log(Button.defaultConfig); // 访问静态属性
const btn = new Button(document.getElementById("btn")!);
3、带注册系统的插件架构
// 插件实例接口
interface Plugin {
init(config: any): void;
execute(): void;
}
// 插件类静态接口
interface PluginConstructor {
new (): Plugin;
readonly pluginName: string;
readonly dependencies: string[];
register(config?: any): void;
}
// 插件管理器
class PluginSystem {
private static plugins = new Map<string, PluginConstructor>();
static register(ctor: PluginConstructor) {
this.plugins.set(ctor.pluginName, ctor);
}
static getPlugin(name: string): PluginConstructor | undefined {
return this.plugins.get(name);
}
}
// 实现一个插件
const LoggerPlugin: PluginConstructor = class LoggerPlugin implements Plugin {
static pluginName = "logger";
static dependencies = ["config"];
static register(config = { level: "info" }) {
PluginSystem.register(this);
// 额外的注册逻辑
}
init(config: any) {
// 初始化
}
execute() {
// 执行日志记录
}
};
// 使用
LoggerPlugin.register({ level: "debug" });
const PluginClass = PluginSystem.getPlugin("logger");
if (PluginClass) {
const plugin = new PluginClass();
}
接口的优势
- 可读性:明确表达数据结构和契约
- 可维护性:集中管理类型定义
- 可扩展性:通过继承和声明合并灵活扩展
- 工具支持:IDE 和 TypeScript 工具能提供更好的智能提示和错误检查
接口是 TypeScript 强大的类型系统的重要组成部分,它帮助开发者在开发阶段捕获潜在错误,同时提供清晰的代码文档。
更新日志
e7112
-1于