02 - 接口与类型别名

0 阅读4分钟

接口(interface)和类型别名(type)是 TypeScript 中定义对象结构的两种核心方式。


2.1 接口(interface)

接口用于描述对象的"形状"——它有哪些属性,每个属性是什么类型:

interface User {
  name: string;
  age: number;
  email: string;
}

// 使用接口
const user: User = {
  name: "张三",
  age: 25,
  email: "zhangsan@example.com",
};

// ❌ 缺少属性会报错
const user2: User = {
  name: "李四",
  // 错误:缺少 age 和 email
};

// ❌ 多余属性也会报错
const user3: User = {
  name: "王五",
  age: 30,
  email: "wangwu@example.com",
  phone: "123456", // 错误:User 中没有 phone
};

2.2 可选属性与只读属性

interface Config {
  host: string;
  port: number;
  readonly database: string;  // 只读,赋值后不可修改
  ssl?: boolean;              // 可选,可以不传
  timeout?: number;           // 可选
}

const config: Config = {
  host: "localhost",
  port: 3306,
  database: "mydb",
};

config.host = "127.0.0.1"; // ✅ 可以修改
config.database = "other";  // ❌ 只读属性不能修改
config.ssl;                 // ✅ undefined(可选属性未赋值)

2.3 索引签名

当你不确定对象有哪些属性,但知道键和值的类型时:

// 字符串索引
interface StringMap {
  [key: string]: string;
}

const headers: StringMap = {
  "Content-Type": "application/json",
  Authorization: "Bearer xxx",
};

// 数字索引
interface NumberArray {
  [index: number]: string;
}

const arr: NumberArray = ["a", "b", "c"];

混合已知属性和索引签名

interface ApiResponse {
  code: number;
  message: string;
  [key: string]: any; // 允许额外的任意属性
}

const res: ApiResponse = {
  code: 200,
  message: "ok",
  data: { id: 1 },
  timestamp: 1234567890,
};

2.4 接口继承(extends)

接口可以继承其他接口,实现复用:

interface Animal {
  name: string;
  age: number;
}

interface Dog extends Animal {
  breed: string;
  bark(): void;
}

const dog: Dog = {
  name: "旺财",
  age: 3,
  breed: "柴犬",
  bark() {
    console.log("汪汪!");
  },
};

多继承

interface HasName {
  name: string;
}

interface HasAge {
  age: number;
}

interface HasEmail {
  email: string;
}

// 同时继承多个接口
interface Person extends HasName, HasAge, HasEmail {
  address?: string;
}

2.5 类型别名(type)

type 可以给任意类型起一个名字:

// 对象类型
type User = {
  name: string;
  age: number;
};

// 原始类型别名
type ID = string | number;

// 函数类型
type Callback = (data: string) => void;

// 元组
type Point = [number, number];

// 字面量联合类型
type Direction = "up" | "down" | "left" | "right";

使用示例

type ID = string | number;

function findUser(id: ID): User | undefined {
  // ...
}

findUser(1);      // ✅
findUser("abc");  // ✅
findUser(true);   // ❌

2.6 interface vs type 的区别

相同点

两者都能描述对象结构:

// interface
interface UserA {
  name: string;
  age: number;
}

// type
type UserB = {
  name: string;
  age: number;
};

// 使用方式完全一样
const a: UserA = { name: "张三", age: 25 };
const b: UserB = { name: "李四", age: 30 };

不同点

1. type 能做但 interface 不能做的事:

// 联合类型
type Status = "active" | "inactive" | "deleted";

// 原始类型别名
type Name = string;

// 元组
type Pair = [string, number];

// 从已有类型计算新类型
type Partial<T> = { [K in keyof T]?: T[K] };

2. interface 能做但 type 不能做的事:

// 声明合并(同名接口会自动合并)
interface Window {
  title: string;
}

interface Window {
  count: number;
}

// 等同于:
// interface Window {
//   title: string;
//   count: number;
// }

3. 扩展方式不同:

// interface 用 extends
interface Animal {
  name: string;
}
interface Dog extends Animal {
  bark(): void;
}

// type 用交叉类型 &
type AnimalType = {
  name: string;
};
type DogType = AnimalType & {
  bark(): void;
};

选择建议

场景推荐
定义对象结构interface(更直观,支持合并)
联合类型、交叉类型type(只能用 type)
给原始类型起别名type
给第三方库扩展类型interface(利用声明合并)
复杂类型计算type

💡 实际开发中:两者差别不大,团队统一风格即可。常见做法是对象用 interface,其他用 type


2.7 接口描述函数

// 方式一:接口
interface SearchFunc {
  (keyword: string, page: number): Promise<string[]>;
}

// 方式二:type(更常用)
type SearchFunc2 = (keyword: string, page: number) => Promise<string[]>;

// 使用
const search: SearchFunc = async (keyword, page) => {
  return ["result1", "result2"];
};

2.8 接口描述类(可调用/可构造)

// 可调用接口
interface Formatter {
  (value: number): string;
}

const format: Formatter = (value) => value.toFixed(2);

// 混合类型:既是函数又有属性
interface Counter {
  (): number;
  count: number;
  reset(): void;
}

function createCounter(): Counter {
  const fn = () => fn.count++ as Counter;
  fn.count = 0;
  fn.reset = () => { fn.count = 0; };
  return fn as Counter;
}

2.9 实用技巧

Record 快速创建映射类型

// 等同于 { [key: string]: number }
type ScoreMap = Record<string, number>;

const scores: ScoreMap = {
  math: 90,
  english: 85,
};

// 限制键的范围
type Role = "admin" | "user" | "guest";
type RolePermissions = Record<Role, string[]>;

const permissions: RolePermissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

用 keyof 获取接口的键

interface User {
  name: string;
  age: number;
  email: string;
}

type UserKeys = keyof User; // "name" | "age" | "email"

function getValue(user: User, key: keyof User) {
  return user[key];
}

📝 练习

  1. 定义一个 Product 接口,包含 id(number)、name(string)、price(number)、description(可选 string)
  2. 定义 VIPProduct 接口继承 Product,添加 discount(number) 属性
  3. 用 type 定义一个 ApiResult 类型,可以是 { success: true; data: any }{ success: false; error: string }
  4. 用索引签名定义一个 Dictionary<T> 泛型类型
// 参考答案

// 1
interface Product {
  id: number;
  name: string;
  price: number;
  description?: string;
}

// 2
interface VIPProduct extends Product {
  discount: number;
}

// 3
type ApiResult =
  | { success: true; data: any }
  | { success: false; error: string };

// 4
type Dictionary<T> = {
  [key: string]: T;
};

const dict: Dictionary<number> = { a: 1, b: 2 };