Skip to content

TypeScript接口与类型别名

接口(Interface)和类型别名(Type Alias)是TypeScript中用于定义复杂类型的两种主要方式。本文将详细介绍它们的用法、区别以及各自的最佳应用场景。

接口基础

接口定义

接口是TypeScript中描述对象形状的一种方式,它定义了对象应该具有的属性和方法:

typescript
// 基本接口定义
interface User {
  id: number;
  name: string;
  email: string;
  age?: number; // 可选属性
  readonly createdAt: Date; // 只读属性
}

// 使用接口
const user: User = {
  id: 1,
  name: "张三",
  email: "zhangsan@example.com",
  createdAt: new Date()
};

// 错误:缺少必需属性
// const invalidUser: User = {
//   name: "李四",
//   email: "lisi@example.com"
// };

// 错误:尝试修改只读属性
// user.createdAt = new Date();

函数接口

接口也可以用来描述函数类型:

typescript
// 函数接口
interface SearchFunction {
  (source: string, subString: string): boolean;
}

// 实现函数接口
const search: SearchFunction = function(source, subString) {
  return source.includes(subString);
};

console.log(search("Hello, world", "world")); // true

可索引类型

接口可以描述可以通过索引访问的类型,如数组或对象:

typescript
// 数组索引接口
interface StringArray {
  [index: number]: string;
}

const myArray: StringArray = ["苹果", "香蕉", "橙子"];
console.log(myArray[0]); // "苹果"

// 对象索引接口
interface Dictionary {
  [key: string]: string;
}

const dict: Dictionary = {
  apple: "苹果",
  banana: "香蕉",
  orange: "橙子"
};

console.log(dict["apple"]); // "苹果"

// 混合索引类型
interface MixedDictionary {
  [index: string]: string | number;
  length: number; // 特定属性
  name: string;   // 特定属性
}

const mixed: MixedDictionary = {
  name: "混合字典",
  length: 3,
  key1: "值1",
  key2: 42
};

接口继承

接口可以继承其他接口,从而扩展其定义:

typescript
// 基础接口
interface Person {
  name: string;
  age: number;
}

// 继承单个接口
interface Employee extends Person {
  employeeId: string;
  department: string;
}

// 继承多个接口
interface Manager extends Employee, Admin {
  subordinates: Employee[];
}

interface Admin {
  privileges: string[];
}

// 使用继承的接口
const manager: Manager = {
  name: "张经理",
  age: 40,
  employeeId: "EMP001",
  department: "研发部",
  privileges: ["系统管理", "用户管理"],
  subordinates: [
    { name: "李工程师", age: 28, employeeId: "EMP002", department: "研发部" }
  ]
};

接口合并

TypeScript允许你多次声明同一个接口,这些声明会被合并:

typescript
// 接口合并
interface Box {
  height: number;
  width: number;
}

interface Box {
  length: number;
  scale: number;
}

// 结果等同于:
// interface Box {
//   height: number;
//   width: number;
//   length: number;
//   scale: number;
// }

const box: Box = {
  height: 10,
  width: 20,
  length: 30,
  scale: 2
};

类型别名基础

类型别名定义

类型别名为现有类型创建新名称,可以用于任何类型,不仅仅是对象类型:

typescript
// 基本类型别名
type ID = string | number;
type Point = { x: number; y: number };
type Callback = (data: string) => void;

// 使用类型别名
const userId: ID = "user123";
const point: Point = { x: 10, y: 20 };
const handleData: Callback = (data) => console.log(data);

联合类型与交叉类型

类型别名常用于创建联合类型和交叉类型:

typescript
// 联合类型
type Status = "pending" | "fulfilled" | "rejected";
type Result<T> = T | Error;

let status: Status = "pending";
status = "fulfilled"; // 有效
// status = "completed"; // 错误:不能将类型"completed"分配给类型Status

// 交叉类型
type Person = {
  name: string;
  age: number;
};

type ContactInfo = {
  email: string;
  phone: string;
};

type Employee = Person & ContactInfo;

const employee: Employee = {
  name: "张三",
  age: 30,
  email: "zhangsan@example.com",
  phone: "123-456-7890"
};

泛型类型别名

类型别名可以使用泛型来创建可重用的类型定义:

typescript
// 泛型类型别名
type Container<T> = { value: T };
type Tree<T> = {
  value: T;
  left?: Tree<T>;
  right?: Tree<T>;
};

// 使用泛型类型别名
const numberContainer: Container<number> = { value: 42 };
const stringContainer: Container<string> = { value: "Hello" };

const tree: Tree<number> = {
  value: 1,
  left: {
    value: 2,
    left: { value: 3 },
    right: { value: 4 }
  },
  right: {
    value: 5
  }
};

条件类型

类型别名可以使用条件类型来基于条件创建不同的类型:

typescript
// 条件类型
type IsArray<T> = T extends any[] ? true : false;
type ExtractType<T> = T extends Array<infer U> ? U : never;

// 使用条件类型
type CheckString = IsArray<string>; // false
type CheckArray = IsArray<string[]>; // true

type NumberArray = number[];
type ExtractedType = ExtractType<NumberArray>; // number

// 更复杂的条件类型
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;

type NestedArray = number[][][];
type FlattenResult = Flatten<NestedArray>; // number

接口与类型别名的对比

语法差异

接口和类型别名在语法上有一些明显的差异:

typescript
// 接口定义
interface User {
  name: string;
  age: number;
}

// 类型别名定义
type User = {
  name: string;
  age: number;
};

功能差异

接口和类型别名在功能上也有一些重要区别:

typescript
// 接口可以被合并,类型别名不能
interface Box {
  height: number;
}
interface Box {
  width: number;
}
// Box现在有height和width属性

// 类型别名不能被合并
// 这会导致错误:重复标识符'Box'
// type Box = { height: number };
// type Box = { width: number };

// 类型别名可以为任何类型创建名称,包括原始类型、联合类型和元组
type Name = string;
type NameOrId = string | number;
type Coordinates = [number, number];

// 接口只能描述对象形状(包括函数和类)
interface Point {
  x: number;
  y: number;
}

// 类型别名可以使用计算属性
type Keys = 'firstname' | 'surname';
type Person = {
  [key in Keys]: string;
};
// 等同于: { firstname: string; surname: string; }

// 接口不能表达映射类型
// 这是不可能的:
// interface Person {
//   [key in Keys]: string;
// }

扩展方式

接口和类型别名的扩展方式不同:

typescript
// 接口扩展接口
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// 接口扩展类型别名
type Animal = {
  name: string;
};

interface Dog extends Animal {
  breed: string;
}

// 类型别名扩展接口
interface Animal {
  name: string;
}

type Dog = Animal & {
  breed: string;
};

// 类型别名扩展类型别名
type Animal = {
  name: string;
};

type Dog = Animal & {
  breed: string;
};

实现与类

类可以实现接口或类型别名,但有一些限制:

typescript
// 类实现接口
interface Animal {
  name: string;
  makeSound(): void;
}

class Dog implements Animal {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  makeSound(): void {
    console.log("汪汪!");
  }
}

// 类实现类型别名(如果类型别名描述的是对象形状)
type Animal = {
  name: string;
  makeSound(): void;
};

class Cat implements Animal {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  makeSound(): void {
    console.log("喵喵!");
  }
}

// 类不能实现联合类型
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; size: number };

// 这会导致错误
// class Circle implements Shape {
//   kind: 'circle' = 'circle';
//   radius: number = 10;
// }

最佳实践

何时使用接口

接口在以下情况下是更好的选择:

  1. 当你需要定义对象、类或函数的形状时
  2. 当你需要声明可合并的API时
  3. 当你需要创建可被类实现的契约时
typescript
// 定义对象形状
interface User {
  id: number;
  name: string;
  email: string;
}

// 定义类的契约
interface Repository<T> {
  findAll(): Promise<T[]>;
  findById(id: string): Promise<T | null>;
  create(item: Omit<T, 'id'>): Promise<T>;
  update(id: string, item: Partial<T>): Promise<T>;
  delete(id: string): Promise<boolean>;
}

// 实现接口
class UserRepository implements Repository<User> {
  async findAll(): Promise<User[]> {
    // 实现代码...
    return [];
  }
  
  async findById(id: string): Promise<User | null> {
    // 实现代码...
    return null;
  }
  
  async create(item: Omit<User, 'id'>): Promise<User> {
    // 实现代码...
    return { id: 1, ...item };
  }
  
  async update(id: string, item: Partial<User>): Promise<User> {
    // 实现代码...
    return { id: parseInt(id), name: '', email: '', ...item };
  }
  
  async delete(id: string): Promise<boolean> {
    // 实现代码...
    return true;
  }
}

何时使用类型别名

类型别名在以下情况下是更好的选择:

  1. 当你需要使用联合类型、交叉类型、元组或其他复杂类型时
  2. 当你需要使用映射类型、条件类型等高级类型特性时
  3. 当你需要为函数签名或简单类型创建别名时
typescript
// 联合类型
type Result<T> = { success: true; value: T } | { success: false; error: Error };

function tryOperation<T>(operation: () => T): Result<T> {
  try {
    return { success: true, value: operation() };
  } catch (error) {
    return { success: false, error: error instanceof Error ? error : new Error(String(error)) };
  }
}

// 映射类型
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

const config: Readonly<{ server: string; port: number }> = {
  server: 'localhost',
  port: 8080
};

// 条件类型
type ArrayElement<T> = T extends Array<infer U> ? U : never;

type Numbers = ArrayElement<number[]>; // number

混合使用

在许多情况下,接口和类型别名可以混合使用,以充分利用它们各自的优势:

typescript
// 使用类型别名定义基本类型
type ID = string | number;
type Status = 'active' | 'inactive' | 'pending';

// 使用接口定义对象形状
interface User {
  id: ID;
  name: string;
  status: Status;
}

// 使用类型别名创建工具类型
type Nullable<T> = T | null;
type UserResponse = Nullable<User>;

// 接口继承类型别名
type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

interface Post extends Timestamped {
  id: ID;
  title: string;
  content: string;
  authorId: ID;
}

高级用例

接口的递归定义

接口可以递归地引用自身,这对于定义树形结构或链表等数据结构很有用:

typescript
// 递归接口
interface TreeNode {
  value: string;
  children?: TreeNode[];
}

const tree: TreeNode = {
  value: "根节点",
  children: [
    {
      value: "子节点1",
      children: [
        { value: "叶节点1" },
        { value: "叶节点2" }
      ]
    },
    {
      value: "子节点2"
    }
  ]
};

类型别名的递归定义

类型别名也可以递归,但需要特殊处理:

typescript
// 递归类型别名
type TreeNode = {
  value: string;
  children?: TreeNode[]; // 直接递归引用
};

// JSON值类型
type JSONValue = 
  | string
  | number
  | boolean
  | null
  | JSONValue[] // 递归数组
  | { [key: string]: JSONValue }; // 递归对象

const json: JSONValue = {
  name: "张三",
  age: 30,
  isAdmin: false,
  address: {
    city: "北京",
    zipCode: "100000"
  },
  hobbies: ["阅读", "游泳", "编程"]
};

接口与类型别名的高级映射

使用映射类型可以基于现有类型创建新类型:

typescript
// 使用接口定义基本类型
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// 使用类型别名和映射类型创建派生类型
type PublicUser = Omit<User, 'password'>;
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;

// 创建具有特定属性的类型
type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'password'
type UserValues = User[keyof User]; // number | string

// 条件映射
type OptionalToNullable<T> = {
  [K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};

interface Options {
  timeout?: number;
  cache?: boolean;
  debug?: boolean;
}

type NullableOptions = OptionalToNullable<Options>;
// 等同于: { timeout: number | null; cache: boolean | null; debug: boolean | null; }

品牌/名义类型

使用接口和类型别名可以创建"品牌类型",这是一种模拟名义类型系统的方式:

typescript
// 使用接口创建品牌类型
interface UserId extends String {
  readonly _brand: unique symbol;
}

interface OrderId extends String {
  readonly _brand: unique symbol;
}

// 创建品牌类型的值
function createUserId(id: string): UserId {
  return id as unknown as UserId;
}

function createOrderId(id: string): OrderId {
  return id as unknown as OrderId;
}

// 使用品牌类型
function processUser(id: UserId) {
  console.log(`处理用户: ${id}`);
}

const userId = createUserId("user-123");
const orderId = createOrderId("order-456");

processUser(userId); // 有效
// processUser(orderId); // 错误:类型不兼容
// processUser("raw-string"); // 错误:类型不兼容

总结

TypeScript中的接口和类型别名是定义和组织类型的强大工具。虽然它们有很多重叠的功能,但各自也有独特的优势:

  • 接口更适合定义对象形状、类契约和API,特别是当你需要声明合并或创建可实现的契约时。
  • 类型别名更适合处理复杂类型组合、联合类型、交叉类型和高级类型操作。

在实际项目中,两者通常会结合使用,以充分利用TypeScript类型系统的强大功能。随着你对TypeScript的深入了解,你将能够更加熟练地选择最适合特定场景的类型定义方式。

用知识点燃技术的火山