Skip to content

TypeScript 类型系统基础:interfacetypeenum 与泛型

很多人学 TypeScript 时,最容易卡住的不是“完全不会写”,而是:

  1. 知道这些关键词见过很多次
  2. 但一到真实项目里,不知道该优先用哪个
  3. 能写出语法,却说不清它到底在解决什么问题

这篇文章专门把最常见、最容易混的几组关键词收口到一条主线里:

  1. interface
  2. type
  3. enum
  4. 泛型

🌟 把这层分工记住:interface 更偏对象契约,type 更偏类型表达和组合,enum 更偏有限常量集合,泛型更偏“同一套逻辑如何在不同类型上复用且不丢类型信息”。`

1. 为什么这些关键词容易混

TypeScript 看起来只是“多写点类型”,但真正落到工程里时,你很快就会遇到这些问题:

  1. 一个对象结构该用 interface 还是 type
  2. 一组有限状态值该用 enum 还是联合字面量类型
  3. 一个通用函数如果想保留输入输出之间的类型关系,该怎么写

如果这些边界没分清,最后常见会变成两种极端:

  1. 什么都用 type
  2. 什么都用 interface

更稳的做法不是背“唯一标准答案”,而是先分清每个关键词最擅长表达什么。

2. interface 到底在解决什么问题

interface 可以看成:用来描述对象结构和契约的一种方式。

它最适合回答的问题是:

  1. 这个对象应该有哪些字段
  2. 这些字段各自是什么类型
  3. 某个类或某组数据需要满足什么结构约束

例如:

ts
interface User {
  id: number
  name: string
  email?: string
}

这段代码表达的是:

  1. User 至少要有 id
  2. name 是必填
  3. email 可以没有

2.1 interface 更适合哪些场景

工程里更常见的场景包括:

  1. 接口返回对象
  2. 组件 props
  3. 类需要实现的契约
  4. 配置对象的结构定义

例如:

ts
interface Payable {
  pay(amount: number): void
}

class OrderService implements Payable {
  pay(amount: number) {
    console.log(`pay ${amount}`)
  }
}

这里 interface 的重点不是“多一个语法糖”,而是:把能力契约说清楚。

2.2 interface 的一个典型特点

interface 很适合不断扩展同一个对象契约。

例如:

ts
interface BaseUser {
  id: number
  name: string
}

interface AdminUser extends BaseUser {
  role: 'admin'
}

它在“描述对象形状”这条线里会显得很自然。

3. type 到底在解决什么问题

type 可以看成:给一个类型表达式起名字。

这句话里最关键的是“类型表达式”。

因为 type 不只会描述对象,它还能描述:

  1. 联合类型
  2. 交叉类型
  3. 元组
  4. 字面量类型
  5. 条件类型
  6. 工具类型组合后的结果

例如:

ts
type RequestStatus = 'idle' | 'loading' | 'success' | 'error'

type Point = [number, number]

type UserWithToken = User & {
  token: string
}

这些写法如果只靠 interface,表达起来就不自然了。

3.1 type 更适合哪些场景

它更适合:

  1. 想给联合类型命名
  2. 想组合多个类型
  3. 想描述元组
  4. 想写更灵活的类型别名

把这层边界拉开看:

  1. interface 更偏“对象契约”
  2. type 更偏“类型表达能力”

3.2 interfacetype 怎么选

不要把它们讲成绝对对立。

更稳的选择方式通常是:

场景更自然的选择
描述对象结构、组件 props、接口契约interface
联合类型、交叉类型、元组、字面量类型别名type
单纯对象结构,团队也统一偏向一种写法二者都可以,但要保持风格一致

🌟 要注意的是,interfacetype 都能描述对象,但 type 的表达范围更大;而对象契约场景下,interface 往往更直观。

4. enum 到底在解决什么问题

enum 可以看成:给一组有限常量值起一组有名字的成员。

例如:

ts
enum OrderStatus {
  Pending = 'PENDING',
  Paid = 'PAID',
  Cancelled = 'CANCELLED',
}

它更像在表达:

  1. 订单状态只有这些值
  2. 每个值都应该有稳定的语义化名字

4.1 enum 的价值是什么

它的价值通常包括:

  1. 常量语义更集中
  2. 代码补全更直接
  3. 比散落的字符串更不容易写错

例如:

ts
function isPaid(status: OrderStatus) {
  return status === OrderStatus.Paid
}

4.2 为什么很多项目又不总是优先用 enum

这是很容易被忽略的边界。

在很多前端项目里,一组有限状态值也经常写成:

ts
type OrderStatus = 'PENDING' | 'PAID' | 'CANCELLED'

这是因为联合字面量类型也能表达“有限取值范围”,而且通常更轻。

所以更稳的理解应该是:

  1. enum 适合需要一组具名常量成员的场景
  2. 如果只是想约束取值范围,联合字面量类型也很常见

工程上很多团队会优先这样选:

  1. 如果重点是“表达有限取值”,优先联合字面量类型
  2. 如果重点是“集中管理一组具名常量”,enum 依然很自然

4.3 不要把 enum 理解成什么

不要把它理解成“所有状态值都必须用它”。

enum 只是 TypeScript 提供的一种有限常量组织方式,不是唯一方案。

5. 泛型到底在解决什么问题

泛型真正要解决的,不是“写复杂语法”,而是:

同一套逻辑需要复用在不同类型上,但复用之后仍然要保留输入和输出之间的类型关系。

例如:

ts
function first<T>(list: T[]): T | undefined {
  return list[0]
}

这里的重点不是 T 这个字母,而是:

  1. 传入 string[] 时,返回值就应该是 string | undefined
  2. 传入 number[] 时,返回值就应该是 number | undefined

如果没有泛型,常见会退化成两种糟糕写法:

  1. 每种类型都复制一份函数
  2. 直接写成 any

5.1 泛型最常见出现在哪

工程里最常见的地方包括:

  1. 函数
  2. 接口
  3. 类型别名
  4. 工具类型
  5. 组件和 Hook 封装

例如:

ts
interface ApiResponse<T> {
  code: number
  message: string
  data: T
}

type UserResponse = ApiResponse<{
  id: number
  name: string
}>

这里 ApiResponse<T> 的价值就在于:外层结构复用,但 data 的具体类型仍然保留下来。

5.2 泛型约束在做什么

泛型不是完全不受限制的。

很多时候你会希望它至少满足某个条件,例如必须有 id 字段:

ts
function getId<T extends { id: number }>(value: T) {
  return value.id
}

这里的 extends 不是在做继承实现,而是在做:

给泛型参数增加一个最小结构约束。

5.3 泛型什么时候不要滥用

如果一段逻辑只会服务一种明确类型,就不要为了“看起来高级”强行抽成泛型。

🌟 泛型应该服务复用和保留类型关系,而不是把简单问题写复杂。

6. 这几个关键词之间的关系怎么串起来

可以用一张表把它们压缩起来:

关键词更偏解决什么
interface描述对象契约和结构
type给类型表达式命名和做类型组合
enum组织有限常量集合
泛型复用逻辑时保留类型信息

如果把它们放进一个真实开发场景里,可以这样理解:

  1. interface 定义接口返回对象结构
  2. type 定义状态值联合、组合类型或工具类型结果
  3. enum 或联合字面量类型表达有限状态
  4. 用泛型把公共函数、响应结构、组件能力做成可复用模板

7. 一个更贴近工程的综合示例

下面这段代码把这几个关键词放在同一个场景里看:

ts
interface User {
  id: number
  name: string
}

type RequestStatus = 'idle' | 'loading' | 'success' | 'error'

enum UserRole {
  Admin = 'ADMIN',
  Member = 'MEMBER',
}

interface PageResult<T> {
  list: T[]
  total: number
}

function getFirstItem<T>(result: PageResult<T>): T | undefined {
  return result.list[0]
}

这里可以直接看到:

  1. Userinterface,因为重点是描述对象结构
  2. RequestStatustype,因为重点是表达有限联合类型
  3. UserRoleenum,因为想把角色常量集中命名
  4. PageResult<T>getFirstItem<T> 用泛型,因为想复用结构和逻辑,同时保留具体类型

8. 一份更实用的选择清单

当你写 TypeScript 时,可以问自己:

  1. 我现在是在描述对象结构吗
  2. 我是在给联合类型、元组或组合类型起名字吗
  3. 我是在组织一组有限常量吗
  4. 我是在做逻辑复用,同时还想保留类型关系吗

对应地可以这样选:

  1. 对象契约优先想 interface
  2. 类型表达和组合优先想 type
  3. 有限常量集合考虑 enum 或联合字面量类型
  4. 复用逻辑且不想丢类型信息时考虑泛型

9. 小结

如果把这篇文章压缩成一句话,可以记住:

interface 更偏对象契约,type 更偏类型表达,enum 更偏有限常量集合,泛型更偏可复用逻辑里的类型保留。真正重要的,不是把这些关键词全背下来,而是知道当前这段代码到底想表达“结构”“范围”“常量”还是“复用”。`

基于 VitePress 构建的个人技术笔记。