深入浅出,带你彻底弄懂 TypeScript 两大类型巨头的恩怨情仇
大家好,我是你们的老朋友FogLetter,今天我们来聊聊 TypeScript 世界里一个经久不衰的话题:type 和 interface,到底用哪个更好?
从 JavaScript 的痛说起
记得我们刚学 JavaScript 的时候,那种自由奔放的感觉吗?
let age = 18;
age = "十八岁"; // 居然不会报错!
这种灵活性在项目小时是优点,但在大型项目中就成了噩梦。想象一下,你定义了一个用户对象,期望它有 name 和 age 属性,结果别人传过来一个带着 username 和 age 的对象,代码在运行时才崩溃,找 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 官方团队的建议:
- 一致性最重要:在项目中保持统一的选择标准
- 考虑团队背景:如果团队有 Java/C# 背景,可能更习惯
interface - 灵活运用:不要死守一种,根据具体场景选择最合适的工具
- 渐进式学习:初学者可以先掌握
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;
总结
type 和 interface 不是竞争对手,而是 TypeScript 类型系统中的两个互补工具。它们各有优势,适用于不同的场景:
interface更适合传统的面向对象编程,特别是需要声明合并和类实现的场景type更适合函数式编程风格,处理复杂的类型运算和组合
就像锤子和螺丝刀,都是工具箱中不可或缺的工具。真正的高手不是固执地只用一种工具,而是根据具体情况选择最合适的那个。
希望这篇笔记能帮助你更好地理解和使用 TypeScript 的类型系统。如果你有更多见解或疑问,欢迎在评论区交流讨论!