Skip to content

接口

概述

接口(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 };

主要区别

  1. 接口可以合并(Declaration Merging):同名接口会自动合并
  2. 类型别名更灵活:可以表示联合类型、交叉类型等
  3. 接口更适合对象结构:接口专门用于定义对象形状
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 类型系统的基础,理解接口对于学习类、泛型等高级概念非常重要
  • 接口检查发生在编译时,不会影响运行时的性能
  • 使用接口可以大大提高代码的可维护性和可读性
  • 接口与类型别名各有优势,根据具体场景选择合适的方式

相关链接

基于 VitePress 构建