接口
概述
接口(Interface)是 TypeScript 中定义对象结构的主要方式之一。接口提供了一种契约机制,用于描述对象应该具有哪些属性和方法,以及它们的类型。通过接口,我们可以在编译时检查对象是否符合预期的结构,从而提高代码的类型安全性和可维护性。接口支持继承、可选属性、只读属性等特性,是构建复杂类型系统的基础。
基本语法
接口使用 interface 关键字定义,后跟接口名称和对象结构:
typescript
// 基本接口定义
interface User {
name: string;
age: number;
email: string;
}
// 使用接口
const user: User = {
name: "John Doe",
age: 30,
email: "john@example.com"
};接口特性
必需属性
默认情况下,接口中定义的所有属性都是必需的,必须提供且类型匹配:
typescript
interface Person {
name: string;
age: number;
}
// ✅ 正确:提供所有必需属性
const person1: Person = {
name: "Alice",
age: 25
};
// ❌ 错误:缺少必需属性 age
const person2: Person = {
name: "Bob"
// Error: Property 'age' is missing in type '{ name: string; }'
};
// ❌ 错误:类型不匹配
const person3: Person = {
name: "Charlie",
age: "30" // Error: Type 'string' is not assignable to type 'number'
};可选属性
使用 ? 标记属性为可选,表示该属性可以存在也可以不存在:
typescript
interface User {
name: string;
age: number;
email?: string; // 可选属性
phone?: string; // 可选属性
}
// ✅ 正确:只提供必需属性
const user1: User = {
name: "Alice",
age: 25
};
// ✅ 正确:提供部分可选属性
const user2: User = {
name: "Bob",
age: 30,
email: "bob@example.com"
};
// ✅ 正确:提供所有属性
const user3: User = {
name: "Charlie",
age: 35,
email: "charlie@example.com",
phone: "123-456-7890"
};提示
访问可选属性时,其值可能是 undefined,需要进行空值检查:
typescript
function getUserEmail(user: User): string {
// 需要检查可选属性是否存在
return user.email ?? "No email provided";
}只读属性
使用 readonly 关键字标记属性为只读,表示该属性在创建后不能被修改:
typescript
interface Config {
readonly apiKey: string;
readonly version: string;
timeout: number; // 普通属性,可以修改
}
// 创建对象
const config: Config = {
apiKey: "abc123",
version: "1.0.0",
timeout: 5000
};
// ✅ 正确:可以修改普通属性
config.timeout = 10000;
// ❌ 错误:不能修改只读属性
config.apiKey = "new-key"; // Error: Cannot assign to 'apiKey' because it is a read-only property
config.version = "2.0.0"; // Error: Cannot assign to 'version' because it is a read-only property注意
readonly 只在编译时生效,运行时仍然可以修改。如果需要真正的不可变对象,可以使用 Object.freeze() 或第三方库。
索引签名
当对象的属性名不确定时,可以使用索引签名(Index Signature)来定义:
typescript
// 使用索引签名定义接口
interface Dictionary {
[key: string]: string; // 索引签名:所有键都是 string,值也是 string
}
const dict: Dictionary = {
"apple": "苹果",
"banana": "香蕉",
"cherry": "樱桃"
};
// 可以动态添加属性
dict["date"] = "日期";
// ❌ 错误:值类型不匹配
dict["number"] = 123; // Error: Type 'number' is not assignable to type 'string'索引签名也可以与其他属性结合使用:
typescript
// 混合使用固定属性和索引签名
interface UserData {
name: string; // 固定属性
age: number; // 固定属性
[key: string]: string | number; // 索引签名:允许其他 string 或 number 类型的属性
}
const userData: UserData = {
name: "John",
age: 30,
city: "Beijing", // 通过索引签名允许
score: 95 // 通过索引签名允许
};注意
当使用索引签名时,固定属性的类型必须是索引签名类型的子类型。例如,如果索引签名是 [key: string]: string | number,那么所有固定属性的类型必须是 string | number 或其子类型。
方法定义
接口可以定义方法,包括普通方法和箭头函数形式:
typescript
interface Calculator {
// 普通方法定义
add(x: number, y: number): number;
// 箭头函数形式
subtract: (x: number, y: number) => number;
// 可选方法
multiply?(x: number, y: number): number;
}
// 实现接口
const calc: Calculator = {
add(x, y) {
return x + y;
},
subtract: (x, y) => x - y,
multiply(x, y) {
return x * y;
}
};
console.log(calc.add(5, 3)); // 8
console.log(calc.subtract(5, 3)); // 2
console.log(calc.multiply?.(5, 3)); // 15(使用可选链调用可选方法)接口继承
接口支持继承,一个接口可以继承一个或多个接口:
typescript
// 基础接口
interface Animal {
name: string;
age: number;
}
// 继承单个接口
interface Dog extends Animal {
breed: string;
bark(): void;
}
// 继承多个接口
interface Cat extends Animal {
color: string;
meow(): void;
}
// 使用继承的接口
const dog: Dog = {
name: "Buddy",
age: 3,
breed: "Golden Retriever",
bark() {
console.log("Woof!");
}
};
const cat: Cat = {
name: "Whiskers",
age: 2,
color: "orange",
meow() {
console.log("Meow!");
}
};接口多重继承
接口可以同时继承多个接口:
typescript
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
// 继承多个接口
interface Duck extends Animal, Flyable, Swimmable {
quack(): void;
}
const duck: Duck = {
name: "Donald",
age: 1,
fly() {
console.log("Flying...");
},
swim() {
console.log("Swimming...");
},
quack() {
console.log("Quack!");
}
};使用示例
示例 1:用户管理系统
typescript
// 定义用户接口
interface User {
id: number;
name: string;
email: string;
age?: number; // 可选属性
readonly createdAt: Date; // 只读属性
preferences?: {
theme: "light" | "dark";
language: string;
};
}
// 创建用户函数
function createUser(
id: number,
name: string,
email: string,
age?: number
): User {
return {
id,
name,
email,
age,
createdAt: new Date() // 只读属性在创建时设置
};
}
// 使用示例
const user = createUser(1, "Alice", "alice@example.com", 25);
console.log(user.name); // "Alice"
console.log(user.age); // 25
console.log(user.createdAt); // 当前日期
// 可以添加可选属性
user.preferences = {
theme: "dark",
language: "zh-CN"
};
// ❌ 错误:不能修改只读属性
// user.createdAt = new Date(); // Error示例 2:API 响应类型
typescript
// 定义 API 响应接口
interface ApiResponse<T> {
status: "success" | "error";
data?: T; // 成功时包含数据
message?: string; // 错误时包含消息
code?: number; // 错误代码
timestamp: number; // 时间戳
}
// 使用泛型接口
function handleResponse<T>(response: ApiResponse<T>): void {
if (response.status === "success" && response.data) {
console.log("Data:", response.data);
} else if (response.status === "error") {
console.log("Error:", response.message, response.code);
}
}
// 创建响应对象
const successResponse: ApiResponse<string> = {
status: "success",
data: "Hello, World!",
timestamp: Date.now()
};
const errorResponse: ApiResponse<string> = {
status: "error",
message: "Not found",
code: 404,
timestamp: Date.now()
};
handleResponse(successResponse); // ✅ 正确
handleResponse(errorResponse); // ✅ 正确示例 3:配置管理系统
typescript
// 定义应用配置接口
interface AppConfig {
readonly appName: string;
readonly version: string;
api: {
baseUrl: string;
timeout: number;
retries?: number; // 可选的重试次数
};
features: {
[featureName: string]: boolean; // 使用索引签名表示动态特性开关
};
}
// 创建配置对象
const config: AppConfig = {
appName: "MyApp",
version: "1.0.0",
api: {
baseUrl: "https://api.example.com",
timeout: 5000,
retries: 3
},
features: {
darkMode: true,
notifications: false,
analytics: true
}
};
// 可以动态添加特性
config.features["newFeature"] = true;
// ❌ 错误:不能修改只读属性
// config.appName = "NewApp"; // Error示例 4:表单验证系统
typescript
// 定义表单数据接口
interface FormData {
username: string;
email: string;
password: string;
confirmPassword?: string; // 可选属性
rememberMe: boolean;
}
// 验证函数
function validateForm(data: FormData): boolean {
// 检查必需字段
if (!data.username || !data.email || !data.password) {
return false;
}
// 验证邮箱格式(简单示例)
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
return false;
}
// 如果提供了确认密码,检查是否匹配
if (data.confirmPassword && data.password !== data.confirmPassword) {
return false;
}
return true;
}
// 使用示例
const formData: FormData = {
username: "john_doe",
email: "john@example.com",
password: "secret123",
confirmPassword: "secret123",
rememberMe: true
};
if (validateForm(formData)) {
console.log("表单验证通过");
} else {
console.log("表单验证失败");
}接口与类型别名
接口和类型别名(Type Alias)在大多数情况下可以互换使用,但有一些区别:
typescript
// 使用接口定义
interface Point {
x: number;
y: number;
}
// 使用类型别名定义
type PointType = {
x: number;
y: number;
};
// 两者使用方式相同
const point1: Point = { x: 10, y: 20 };
const point2: PointType = { x: 10, y: 20 };主要区别
- 接口可以合并(Declaration Merging):同名接口会自动合并
- 类型别名更灵活:可以表示联合类型、交叉类型等
- 接口更适合对象结构:接口专门用于定义对象形状
typescript
// 接口合并示例
interface Window {
title: string;
}
interface Window {
width: number;
}
// 合并后的 Window 接口包含 title 和 width
const window: Window = {
title: "My Window",
width: 800
};信息
关于接口和类型别名的详细区别,请参考类型别名章节。
类型检查示例
常见错误
typescript
// ❌ 错误:缺少必需属性
interface User {
name: string;
age: number;
}
const user: User = {
name: "John"
// Error: Property 'age' is missing in type '{ name: string; }'
};
// ❌ 错误:类型不匹配
const user2: User = {
name: "John",
age: "30" // Error: Type 'string' is not assignable to type 'number'
};
// ❌ 错误:多余的属性(在严格模式下)
const user3: User = {
name: "John",
age: 30,
email: "john@example.com" // Error: Object literal may only specify known properties
};
// ❌ 错误:修改只读属性
interface Config {
readonly apiKey: string;
}
const config: Config = {
apiKey: "abc123"
};
config.apiKey = "new-key"; // Error: Cannot assign to 'apiKey' because it is a read-only property正确写法
typescript
// ✅ 正确:提供所有必需属性
interface User {
name: string;
age: number;
email?: string; // 可选属性
}
const user: User = {
name: "John",
age: 30
};
// ✅ 正确:提供可选属性
const user2: User = {
name: "John",
age: 30,
email: "john@example.com"
};
// ✅ 正确:使用索引签名允许额外属性
interface FlexibleUser {
name: string;
age: number;
[key: string]: string | number;
}
const user3: FlexibleUser = {
name: "John",
age: 30,
email: "john@example.com", // 通过索引签名允许
score: 95
};注意事项
提示
- 接口是结构化的(Structural Typing),只要对象的结构匹配接口定义,就可以赋值,不需要显式实现接口
- 使用可选属性
?时,在访问属性前应该检查属性是否存在,避免运行时错误 - 只读属性
readonly只在编译时生效,运行时仍然可以修改(需要使用Object.freeze()实现真正的不可变) - 接口支持继承,可以创建接口层次结构
注意
- 在严格模式下,对象字面量不能包含未在接口中声明的属性(除非使用索引签名)
- 索引签名的类型必须包含所有固定属性的类型,否则会出现类型错误
- 可选属性在访问时可能是
undefined,需要进行空值检查 - 接口合并(Declaration Merging)可能导致意外的行为,需要谨慎使用
信息
接口在以下场景特别有用:
- 定义 API 契约和数据结构
- 创建可复用的类型定义
- 实现面向对象编程中的接口概念
- 与类一起使用,定义类的结构契约
重要
- 接口是 TypeScript 类型系统的基础,理解接口对于学习类、泛型等高级概念非常重要
- 接口检查发生在编译时,不会影响运行时的性能
- 使用接口可以大大提高代码的可维护性和可读性
- 接口与类型别名各有优势,根据具体场景选择合适的方式
相关链接
- 对象类型 - 了解对象类型的基础概念
- 类型别名 - 学习类型别名的用法和与接口的区别
- 类基础 - 了解如何在类中实现接口
- 泛型 - 学习泛型接口的使用
- TypeScript 官方文档 - 接口