你真的了解 TypeScript 中 interface 和 type 的区别吗❓❓❓

293 阅读6分钟

前言

在日常TypeScript规范下的开发场景中,interfacetype是我们经常用来定义数据类型的两种方式。
在使用效果上,二者并没有太大的区别,也导致平时我们往往忽视了它们的内在差异;另一方面,对于使用TypeScript不太熟练的小伙伴,经常在项目代码或一些第三方库源码中也能看到这两种形式的各种混用方式,不禁容易感到困惑:到底该使用哪一种?何时使用?今天带大家一起深入了解其中的细节。

一、基本语法形式

在 TypeScript 中,interfacetype 都能用来定义类型。 例如,定义一个用户User的类型:

1.1 基本语法

// interface 定义
interface User {
  id: number;
  name: string;
  isActive?: boolean; // 可选属性
  readonly createdAt: Date; // 只读属性
}

// type 定义
type User = {
  id: number;
  name: string;
  isActive?: boolean;
  readonly createdAt: Date;
};

1.2 类型支持

上面的定义是对我们自定义的数据结构进行类型约束,除了这种定义方式以外,在有些情况下,我们希望能够基于原始类型进行扩展。type就提供了灵活的类型定义方式:

  • Type:更灵活,可定义原始类型、元组、联合类型、映射类型等。
type ID = string | number; // 联合类型
type Point = [number, number]; // 元组
type Primitive = string; // 原始类型别名
  • Interface:主要用于定义对象结构,支持可选属性、只读属性和函数类型,但无法直接定义原始类型、元组或联合类型。

函数类型条件类型

// 函数类型(两种写法)
interface AddFunc { (a: number, b: number): number; }
type AddType = (a: number, b: number) => number; 
 
// 条件类型(仅 type 支持)
type IsString<T> = T extends string ? true : false; 

Type:在定义函数类型和工具类型时更灵活,更擅长定义复杂类型组合

二、深入功能对比

2.1 声明合并能力

  • Interface:支持声明合并。多次定义同名接口时,TypeScript 会自动合并它们的成员。
  • Type:不允许重复声明同名类型,会直接报错。
// interface 自动合并同名声明
interface Window {
  myCustomProperty: string;
}

// 在另一个文件中
interface Window {
  anotherProperty: number;
}

// 最终 Window 类型包含两个属性
const win: Window = {
  myCustomProperty: "test",
  anotherProperty: 42
};

// type 会报错:Duplicate identifier 'User'
type User = { id: number };
type User = { name: string }; // Error!

应用场景:当需要扩展第三方库类型或模块化定义对象结构时,interface 的声明合并特性非常有用。例如,为现有类型添加自定义属性。

2.2 类型扩展机制

  • Interface:使用 extends 关键字扩展其他接口或类型,包括类class
  • Type:通过交叉类型(&)组合多个类型,但无法直接扩展类class
// interface 继承
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// type 交叉类型
type Animal = { name: string };
type Dog = Animal & { breed: string };

需要注意:

interface 继承时当子接口通过 extends 继承父接口时,子接口的实现必须同时满足父接口和子接口的所有必选属性。且可以重新声明父接口的属性,但新定义的属性类型必须是父接口类型的 子类型或相同类型,否则会导致类型冲突。

interface Parent {
  role: string;
}
 
interface Child extends Parent {
  role: "admin" | "user";  // ✅ 合法:字面量类型是string 的子类型
}
 
interface Child extends Parent {
  role: number;  // ❌ 错误:number 无法赋值给 string
}

type 交叉类型会合并同名属性,可能导致逻辑错误(如 string & number 为 never)。

interface A { prop: string; }
interface B extends A { prop: number; } // Error: 类型冲突 
 
type C = { prop: string; } & { prop: number; }; // prop: never 

2.3 类实现(Class Implements)

类**class**可通过 implements 实现 interface 或 type,但无法实现联合类型的 type

// 使用interface
interface Animal {
  name: string;
  makeSound(): void;
}
// 使用type
type Animal = {
  name: string;
  makeSound(): void;
};

// ✅ 均可被类实现
class Dog implements Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  makeSound() {
    console.log("Woof!");
  }
}
// 联合类型下的type
type UnionPerson = { name: string; } | { age: number; };
// ❌ 无法实现联合类型
class Employee implements UnionPerson { /* Error! */ } 

补充说明:关于implements关键字

  • interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。

implements 的作用

  1. 强制类型检查
    • 编译器验证:TypeScript 会在编译时检查类是否完整实现了接口的所有 必选属性/方法(包括类型一致性)。
    • 错误提前暴露:若类与接口的成员不匹配(缺少属性、类型不符),编译阶段立即报错,避免运行时潜在问题。
  2. 代码约定与文档化
    • 明确契约:通过接口定义类必须具备的能力,例如 Country 必须包含 name 和 capital。
    • 提高可读性:开发者一眼可知类的核心功能,例如 MyCountry 需遵循 Country 的规范。

三、最佳实践建议

3.1 何时选择 interface

  1. 定义对象结构:当主要描述对象结构时
  2. 需要声明合并:如扩展第三方库类型
  3. 类实现接口:使用 implements 关键字(与类继承机制更契合)
// 定义对象结构并需要扩展
interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
}

// 扩展第三方库类型
declare module 'axios' {
  interface AxiosRequestConfig {
    myCustomHeader?: string;
  }
}

// 类实现或方法签名
...

3.2 何时选择 type

  1. 基本类型别名:如 type ID = string | number
  2. 联合/交叉类型:如 type Status = 'pending' | 'completed'
  3. 映射类型/条件类型:如 type Readonly<T> = { readonly [P in keyof T]: T[P] }
// 使用 type 定义复杂状态联合类型
type LoadingState = { status: 'loading' };
type ErrorState = { status: 'error'; error: Error };
type SuccessState<T> = { status: 'success'; data: T };

type AsyncState<T> = LoadingState | ErrorState | SuccessState<T>;

四、总结

4.1 关键差异与使用场景总览

关键差异:

特性interfacetype
合并能力✅ 支持声明合并(同名接口自动合并)❌ 不支持声明合并
扩展方式✅ 支持继承(extends)✅ 支持交叉类型(&)
类型别名❌ 不能定义基本类型别名✅ 可以定义基本类型别名
元组❌ 不支持✅ 支持
映射类型/条件类型❌ 不支持,静态结构,只能定义已知、固定的属性结构✅ 支持

使用场景:

选择依据推荐方案示例场景
对象结构描述interface组件 Props 定义
类型别名/联合类型type状态管理中的联合类型
需要声明合并interface扩展第三方库类型
复杂类型操作type映射类型、条件类型

4.2 总结

在大多数业务代码中,优先使用 interface 定义对象结构,保持代码一致性;当需要复杂类型操作时,使用 type 的灵活性。这种组合使用方式能充分发挥 TypeScript 的类型系统优势。

通过理解这些差异,有助于我们编写出更清晰、更高效的 TypeScript 代码,同时避免常见的类型设计陷阱。