Type 与 Interface 的终极对决:谁才是 TypeScript 的类型之王?

66 阅读6分钟

深入浅出,带你彻底弄懂 TypeScript 两大类型巨头的恩怨情仇

大家好,我是你们的老朋友FogLetter,今天我们来聊聊 TypeScript 世界里一个经久不衰的话题:typeinterface,到底用哪个更好?

从 JavaScript 的痛说起

记得我们刚学 JavaScript 的时候,那种自由奔放的感觉吗?

let age = 18;
age = "十八岁"; // 居然不会报错!

这种灵活性在项目小时是优点,但在大型项目中就成了噩梦。想象一下,你定义了一个用户对象,期望它有 nameage 属性,结果别人传过来一个带着 usernameage 的对象,代码在运行时才崩溃,找 bug 找到怀疑人生。

TypeScript 的出现就是为了解决这个问题,它给 JavaScript 戴上了类型的"枷锁",让错误在编写阶段就暴露无遗。

初识两大主角

Interface:面向对象的正统血脉

interface 这个词对于从 Java、C# 等语言转来的开发者来说再熟悉不过了。它就像是一份契约,规定了实现它的类必须满足的条件。

interface User {
  name: string;
  age: number;
  sayHello?: () => void; // 可选方法
}

const user: User = {
  name: "张三",
  age: 18,
  sayHello: () => {
    console.log("你好!");
  },
};

Type:灵活多变的类型魔术师

type(类型别名)则更加灵活,它就像是给类型起了一个别名,让复杂的类型变得简单易懂。

type UserType = {
  name: string;
  age: number;
  sayHello?: () => void;
};

const user: UserType = {
  name: "李四",
  age: 20,
};

看到这里,你可能会想:"这不差不多吗?" 别急,好戏还在后头!

相同点:兄弟般的相似

1. 都可以描述对象结构

这是它们最基础的共同点:

// interface 方式
interface Point {
  x: number;
  y: number;
}

// type 方式
type PointType = {
  x: number;
  y: number;
};

const point1: Point = { x: 1, y: 2 };
const point2: PointType = { x: 1, y: 2 };

2. 都支持函数类型定义

// interface 定义函数
interface SearchFunc {
  (source: string, subString: string): boolean;
}

// type 定义函数
type SearchFuncType = (source: string, subString: string) => boolean;

const mySearch: SearchFunc = (src, sub) => {
  return src.includes(sub);
};

3. 都支持扩展

虽然语法不同,但都能实现类型的扩展:

// interface 扩展
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// type 扩展
type AnimalType = {
  name: string;
};

type DogType = AnimalType & {
  breed: string;
};

不同点:各显神通的舞台

现在来到最精彩的部分!让我们看看它们在哪些地方展现出独特的个性。

1. 继承方式:extends vs &

这是最明显的语法差异:

// interface 使用 extends
interface Person {
  name: string;
}

interface Employee extends Person {
  job: string;
}

// type 使用 &(交叉类型)
type PersonType = {
  name: string;
};

type EmployeeType = PersonType & {
  job: string;
};

& 符号在 TypeScript 中叫做交叉类型,意思是把多个类型合并成一个类型,新类型拥有所有类型的特性。

2. 声明合并:interface 的独门绝技

这是 interface 最强大的特性之一:

interface Car {
  brand: string;
}

interface Car {
  speed: number;
}

// 两个 Car 接口会自动合并!
const myCar: Car = {
  brand: "Toyota",
  speed: 200,
};

这个特性在处理第三方库类型扩展时特别有用:

// 假设我们使用了某个 UI 库
interface ButtonProps {
  size: "small" | "medium" | "large";
}

// 后来我们想在项目中扩展这个接口
interface ButtonProps {
  theme: "primary" | "secondary";
}

// 现在 ButtonProps 自动包含了 size 和 theme
const props: ButtonProps = {
  size: "medium",
  theme: "primary",
};

type 不允许重复声明:

type CarType = {
  brand: string;
};

// ❌ 错误:重复标识符 'CarType'
type CarType = {
  speed: number;
};

3. 应用范围:type 的全面性

type 能做的事情更多:

定义基本类型别名:

type ID = string | number;
type Maybe<T> = T | null | undefined;

定义联合类型:

type Status = "pending" | "success" | "error";
type Shape = Circle | Square | Triangle;

定义元组类型:

type Point3D = [number, number, number];
type StringNumberPair = [string, number];

从已有类型中提取子集:

type User = {
  id: number;
  name: string;
  email: string;
  age: number;
};

// 只选取 name 和 email
type UserContactInfo = Pick<User, "name" | "email">;

// 排除某些属性
type UserWithoutId = Omit<User, "id">;

4. 类实现:interface 的传统优势

interface 可以被类实现,这是面向对象编程的经典模式:

interface Serializable {
  serialize(): string;
}

class Person implements Serializable {
  constructor(public name: string, public age: number) {}

  serialize(): string {
    return JSON.stringify(this);
  }
}

虽然 type 也可以被类实现,但这并不是推荐的做法:

type SerializableType = {
  serialize(): string;
};

// 可以,但不推荐
class AnotherPerson implements SerializableType {
  serialize(): string {
    return "serialized";
  }
}

性能考量:一个被忽视的细节

在大型项目中,性能差异可能会显现出来。一般来说:

  • interface 的检查性能通常更好,因为 TypeScript 可以更高效地处理接口继承
  • 复杂的 type 类型,特别是涉及大量条件类型或递归类型时,可能会影响编译速度

不过对于大多数应用来说,这种差异可以忽略不计。

实战中的选择策略

经过多年的 TypeScript 实战,我总结出了以下选择策略:

优先使用 interface 的情况:

1. 面向对象设计:

// 定义抽象接口
interface Repository<T> {
  findById(id: string): T | undefined;
  save(entity: T): void;
}

// 具体实现
class UserRepository implements Repository<User> {
  findById(id: string): User | undefined {
    // 实现查找逻辑
  }
  
  save(user: User): void {
    // 实现保存逻辑
  }
}

2. 需要声明合并的库类型定义:

// 为第三方库扩展类型
declare module "some-library" {
  interface Config {
    customOption?: boolean;
  }
}

3. 清晰的继承层次:

interface Entity {
  id: string;
  createdAt: Date;
}

interface User extends Entity {
  name: string;
  email: string;
}

interface Product extends Entity {
  title: string;
  price: number;
}

优先使用 type 的情况:

1. 简单的类型别名:

type URLString = string;
type milliseconds = number;

2. 联合类型和交叉类型:

type Result = Success | Error;
type AdminUser = User & { permissions: string[] };

3. 复杂的类型运算:

// 条件类型
type IsString<T> = T extends string ? true : false;

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

// 模板字面量类型
type EventName = `on${string}Click`;

4. 元组类型:

type UseStateResult<T> = [T, (value: T) => void];

现代 TypeScript 的最佳实践

根据 TypeScript 官方团队的建议:

  1. 一致性最重要:在项目中保持统一的选择标准
  2. 考虑团队背景:如果团队有 Java/C# 背景,可能更习惯 interface
  3. 灵活运用:不要死守一种,根据具体场景选择最合适的工具
  4. 渐进式学习:初学者可以先掌握 interface,再逐步学习 type 的高级特性

我个人推荐的实践是:

// 对象结构使用 interface
interface Props {
  title: string;
  onClick: () => void;
}

// 联合类型、元组等使用 type
type ButtonSize = "small" | "medium" | "large";
type ModalState = [boolean, (open: boolean) => void];

// 工具类型和复杂类型运算使用 type
type PartialUser = Partial<User>;
type UserKeys = keyof User;

总结

typeinterface 不是竞争对手,而是 TypeScript 类型系统中的两个互补工具。它们各有优势,适用于不同的场景:

  • interface 更适合传统的面向对象编程,特别是需要声明合并和类实现的场景
  • type 更适合函数式编程风格,处理复杂的类型运算和组合

就像锤子和螺丝刀,都是工具箱中不可或缺的工具。真正的高手不是固执地只用一种工具,而是根据具体情况选择最合适的那个。

希望这篇笔记能帮助你更好地理解和使用 TypeScript 的类型系统。如果你有更多见解或疑问,欢迎在评论区交流讨论!