前言
TypeScript 中最容易让初学者困惑的两个问题:type 和 interface 到底有什么区别?import type 又是什么,为什么导入一个类型还要专门加个 type 关键字?
这两个问题看似简单,但背后涉及 TypeScript 类型系统的核心设计理念:类型只在编译时存在,运行时全部消失。本文从语法能力、编译器行为、性能差异、使用场景四个维度,把这两个话题讲透。
一、type vs interface:不只是"都能描述对象"
1.1 先看两个基础定义
typescript
// interface --- 描述对象的结构
interface User {
name: string;
age: number;
}
// type --- 同样可以描述对象的结构
type User = {
name: string;
age: number;
}
多数教程到这里就停了:"描述对象用 interface,其他用 type"。但真实世界的问题远比这复杂。
1.2 能力全集:谁都能做什么
typescript
// ─── 泛型:两者都支持 ───
interface List<T> { items: T[]; total: number }
type List<T> = { items: T[]; total: number }
// ─── 可选 / 只读:两者都支持 ───
interface Config {
readonly apiKey: string;
timeout?: number;
retry: number;
}
type Config = {
readonly apiKey: string;
timeout?: number;
retry: number;
}
// ─── 函数类型:两者都支持 ───
interface Fn { (x: number): string; description: string }
type Fn = { (x: number): string; description: string }
// ─── 索引签名:两者都支持 ───
interface Dictionary { [key: string]: number }
type Dictionary = { [key: string]: number }
// ─── 类实现:两者都支持 ───
interface Speakable { speak(): void }
type Speakable = { speak(): void }
class Person implements Speakable {
speak() { console.log('hello') }
}
结论:在"描述对象结构"这件事上,type 和 interface 能力几乎完全重叠。
1.3 interface 独占的能力
声明合并(Declaration Merging)--- 这是 interface 最不可替代的特性
typescript
// 同名 interface 会自动合并
interface User {
name: string;
}
interface User {
age: number;
}
// 最终的 User:{ name: string; age: number }
// type 别名做不到这一点,重复定义会直接报错
// 这个特性最重要的应用场景:扩展第三方库的类型
// 比如给全局 Window 上加一个自定义属性:
declare global {
interface Window {
__CUSTOM_CONFIG__: { env: string }
}
}
// 项目中任何地方都不用单独 import,直接用 window.__CUSTOM_CONFIG__
// 再比如扩展 express 的 Request 类型(给 req 加 user 字段):
declare global {
namespace Express {
interface Request {
user?: UserInfo
}
}
}
extends 的语法优势
typescript
// 从多个接口继承,interface 的写法更直观
interface Animal { name: string }
interface CanRun { run(): void }
interface CanSwim { swim(): void }
interface Dog extends Animal, CanRun, CanSwim { breed: string }
// 等价于 type 的交叉类型写法
type Dog = Animal & CanRun & CanSwim & { breed: string }
// 虽然效果一样,但 extends 的语义更清晰:"Dog 是 Animal 的一种,且能跑能游"
1.4 type 独占的能力
这是 type 真正发力的领域------以下所有事情 interface 都做不到:
typescript
// 1. 联合类型(Union Type)------ type 最常见的用例
type Status = 'loading' | 'success' | 'error'
type ID = string | number
type Theme = 'light' | 'dark' | 'system'
// 2. 元组------定义"固定长度、固定类型"的数组
type Point2D = [number, number]
type ApiResponse = [number, string, Record<string, unknown>] // [code, message, data]
// 3. 函数签名的简写形式
type OnChange = (value: string) => void
type Predicate<T> = (item: T) => boolean
type AsyncTask = () => Promise<void>
// 4. 映射类型(Mapped Types)
// 这是 TypeScript 最强大的特性之一,interface 完全做不到
type Readonly<T> = { readonly [K in keyof T]: T[K] }
type Nullable<T> = { [K in keyof T]: T[K] | null }
type PickOptional<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K] }
// 5. 条件类型(Conditional Types)
// 类型层面的 if/else
type IsString<T> = T extends string ? true : false
type ExtractPromise<T> = T extends Promise<infer U> ? U : T
// 6. 从值推导类型
const colors = ['red', 'green', 'blue'] as const
type Color = typeof colors[number] // 'red' | 'green' | 'blue'
// 这在定义"所有合法颜色值"的联合类型时非常实用
// 7. 模板字面量类型
type EventName<T extends string> = `on${Capitalize<T>}`
// EventName<'click'> = 'onClick'
type FileType = `${string}.${'jpg' | 'png' | 'svg'}`
// 约束一个字符串必须符合特定模式
// 8. 工具类型的组合使用
// 把一个 interface 的所有属性变成可选的、只读的、可空的...
interface UserDto {
id: number; name: string; email: string; createdAt: Date
}
type UserPartial = Partial<UserDto> // 所有属性可选
type UserReadonly = Readonly<UserDto> // 所有属性只读
type UserUpdate = Partial<Pick<UserDto, 'name' | 'email'>> // 只允许改名字和邮箱
1.5 编译器行为差异
这是很多人不知道但其实很关键的区别:
typescript
// 差异一:编译器报错信息的"展开程度"
interface IUser { name: string; age: number }
type TUser = { name: string; age: number }
function handleUser(user: IUser) { }
handleUser({ name: 'Tom' })
// 报错信息:Type '{ name: string; }' is not assignable to type 'IUser'
// Property 'age' is missing in type '{ name: string; }'
// ↑ 直接告诉你"IUser 缺了 age",清晰明了
function handleTUser(user: TUser) { }
handleTUser({ name: 'Tom' })
// 报错信息:Type '{ name: string; }' is not assignable to type '{ name: string; age: number; }'
// Property 'age' is missing in type '{ name: string; }'
// ↑ 展开成了原始形状,复杂类型展开后会很长,淹没真正的错误信息
typescript
// 差异二:自引用的行为
// 在 interface 中自引用更自然
interface TreeNode {
value: string;
children: TreeNode[]; // ✅ 直接引用自己
}
// 在 type 中同样可以,但复用交叉类型时会遇到麻烦
type TreeNode = {
value: string;
children: TreeNode[];
}
// 如果是:type Named = { name: string } & { children: Named[] }
// 某些 TS 版本下交叉类型的自引用可能报错
1.6 编译性能差异
typescript
// 在大项目中(1000+ 文件),interface 的编译通常快于 type
// 原因在于求值策略:
// interface → 惰性求值(Lazy Evaluation)
// 只在类型检查用到时才计算,且结果缓存
// type → 即时展开(Eager Expansion)
// 定义时就展开整个类型结构,复杂的交叉/条件类型展开代价大
// 举个例子:
interface A { a: string }
interface B { b: number }
interface C extends A, B { c: boolean } // 惰性:用到 C 的 c 时才去查 A 和 B
type A = { a: string }
type B = { b: number }
type C = A & B & { c: boolean } // 即刻:定义 C 时就展开所有属性
// 这也是为什么 TypeScript 官方手册推荐:
// "Most of the time, you can use either. Use interface until you need features from type."
1.7 决策树:什么时候用哪个
go
需要定义什么?
│
├─ 对象的形状(API 响应体、组件 Props、数据库 Model...)
│ → interface(首选)
│ 理由:更好的错误提示、可 extends、声明合并、编译快
│
├─ 联合类型、"或"关系
│ → type
│ 理由:interface 做不到
│
├─ 函数签名
│ → type
│ 理由:type Fn = (x: A) => B 比 interface 紧凑
│
├─ 元组(固定长度的数组)
│ → type
│ 理由:interface 做不到
│
├─ 需要扩展第三方库的类型
│ → interface
│ 理由:声明合并不需要改源码
│
├─ 映射类型 / 条件类型 / 工具类型
│ → type
│ 理由:interface 完全不支持
│
├─ 模板字面量类型
│ → type
│ 理由:interface 不支持
│
└─ 不确定?
→ 默认用 interface,遇到它做不到的事情再换 type
(TypeScript 官方团队也推荐这个策略)
1.8 常见误区澄清
typescript
// ❌ 误区 1:"interface 只能定义对象形状"
// 事实:interface 也可以定义函数
interface Comparator<T> {
(a: T, b: T): number // 调用签名
}
const byAge: Comparator<Person> = (a, b) => a.age - b.age
// ❌ 误区 2:"type 不能 extends"
// 事实:用交叉类型 & 可以达到同样效果
type Animal = { name: string }
type Dog = Animal & { breed: string } // Dog 包含 name 和 breed
// ❌ 误区 3:"class 不能 implements type"
// 事实:可以,只要 type 描述的是对象形状
type Shape = { area(): number; perimeter(): number }
class Rectangle implements Shape { /* ... */ }
// ❌ 误区 4:"interface 和 type 有运行时的差异"
// 事实:两者都只在编译时存在,编译成 JS 后都被删除
// 它们在运行时没有任何区别------因为根本不存在
二、import type:类型导入的本质
2.1 问题起源
TypeScript 的核心设计理念是类型擦除 。所有类型标注在编译成 JavaScript 后都被删除。但 import 语句有一个微妙的问题:
typescript
// 假设 model.ts 里只导出一个 interface:
export interface User {
id: number;
name: string;
}
// component.tsx
import { User } from './model' // ← 编译后这行会保留
// 但 ./model.ts 编译后是空文件!(interface 被删了)
// JavaScript 运行时去加载一个空模块,虽然有兼容机制,但终归是浪费
// 更严重的是:如果 tsconfig 配置 strict,TS 甚至不让你这样写
2.2 import type 做什么
typescript
// 加上 type --- 告诉编译器"这只是一个类型"
import type { User } from './model'
// 编译后的 JavaScript:
// (整行被删除,什么都不剩)
核心规则:
typescript
// ✅ 导入的只是类型(interface / type 别名) → 加 type
import type { User, Order, Product } from '../types/models'
import type { VNode } from 'react'
// ❌ 导入的是运行时的值(函数、变量、类、组件) → 不加 type
import { useState, useEffect } from 'react'
import { fetchUsers } from '../api/userApi'
// ✅ 混用(TypeScript 4.5+ 内联语法)
import { fetchUsers, type User } from '../api/userApi'
// ↑ 值保留 ↑ 类型删除
2.3 为什么 import type 是刚需
原因一:打破循环依赖。 这是 import type 最重要的工程价值。
typescript
// ❌ 没有 import type 时的循环依赖
// types/user.ts
import { Order } from './order' // 引用了 order 模块
export interface User {
id: number;
orders: Order[];
}
// types/order.ts
import { User } from './user' // 又引用了 user 模块
export interface Order {
id: number;
buyer: User;
}
// → 循环依赖!打包工具可能报错,运行时可能出现 undefined
// ✅ 用 import type 解决
// types/user.ts
import type { Order } from './order' // 类型导入,不参与模块依赖图!
export interface User {
id: number;
orders: Order[];
}
// types/order.ts
import type { User } from './user' // 类型导入
export interface Order {
id: number;
buyer: User;
}
// → 两个文件互相引用对方类型,但编译后没有任何 import 语句
// → 循环依赖从根源上被消除
在大型项目中,模块之间的循环依赖是常见问题,import type 是最优雅的解法。
原因二:避免无效的模块加载。
javascript
// 不加 type:编译后的 JS 中有 /* harmony import */ var model = __webpack_require__('./model')
// 但这个模块只有一个 interface,运行时根本不需要它 → 浪费
// 加 type:编译后直接删除,JS 中没有任何痕迹
原因三:Tree-shaking 的安全性。
打包工具对"这个 import 到底是值还是类型"的判断不稳定。import type 给了它明确的指令,不会出现"应该被摇掉的类型模块被意外保留"的情况。
2.4 语法演变(TypeScript 3.8 → 4.5)
typescript
// TS 3.7 之前:没有 import type,只能用普通 import
import { User } from './model' // 可能有问题
// TS 3.8:引入 import type 语法
import type { User } from './model'
// TS 4.5:内联 type 语法------同一个语句混用值和类型
import { getUser, type User } from './api'
// 简洁且语义明确
// TS 5.0:type 修饰符可以用于 export
export type { User, Order } // 只导出类型,不影响运行时
2.5 import type 的逻辑模型
go
普通 import:
编译器 → 在 JS 产物中保留 import 语句 → 运行时加载模块 → 获取导出的值 → 使用
import type:
编译器 → 读取类型定义 → 做类型检查 → 通过后 → ✂️ 整行删除
产物 → 没有 import 语句 → 没有模块加载 → 零运行时开销
关键认知:
import type 导入的不是"模块",而是"类型信息"。
类型信息像建筑图纸------盖房子时(编译时)必须看,
房子盖好入住后(运行时)就不需要了。
2.6 import type 和 export type 使用场景总结
typescript
// 场景 1:跨模块引用 interface
// user/types.ts
export interface User { id: number; name: string }
// order/types.ts
import type { User } from '../user/types' // ✅ type import
export interface Order { buyer: User }
// 场景 2:React 组件的 Props 类型
// model/color.ts
export interface Color { red: number; green: number; blue: number }
// components/ColorDisplay.tsx
import type { Color } from '../model/color' // ✅ type import
interface Props { color: Color }
const ColorDisplay: React.FC<Props> = (props) => { /* ... */ }
// 场景 3:统一的类型入口(barrel export)
// types/index.ts
export type { User } from './user'
export type { Order } from './order'
export type { Product } from './product'
// 其他文件:import type { User, Order } from '../types'
// 场景 4:循环依赖的救星
// 当 A 的类型依赖 B,B 的类型依赖 A 时,
// 至少其中一方用 import type,循环就能被打破
三、核心认知:类型只在编译时存在
理解 TypeScript 类型系统,最关键的认知就是这一句话。用一个完整的例子串起来:
编译前(TypeScript 源文件):
typescript
import type { User } from './model' // type-only import
interface Props { // interface 定义
user: User
onUpdate: (user: User) => void
}
const UserCard: React.FC<Props> = ({ user, onUpdate }) => {
const fullName = `${user.firstName} ${user.lastName}` // 用到了 user 的属性
return (
<div onClick={() => onUpdate(user)}>
<h3>{fullName}</h3>
</div>
)
}
编译后(JavaScript 产物):
javascript
// import type { User } from './model' ✂️ 删除
// interface Props { ... } ✂️ 删除
const UserCard = ({ user, onUpdate }) => {
const fullName = user.firstName + " " + user.lastName
return React.createElement('div',
{ onClick: () => onUpdate(user) },
React.createElement('h3', null, fullName)
)
}
所有跟类型相关的三样东西------import type、interface、泛型标注 React.FC<Props>------都在编译后消失了。但它们的作用在编译时已经完成:确保了 user.firstName 存在、onUpdate 的参数类型正确、组件接收的 props 符合 Props 定义。
TypeScript 的价值在编译时兑现,JavaScript 的运行不需要赘余的类型信息。
四、总结
type vs interface
| 维度 | interface |
type |
|---|---|---|
| 描述对象形状 | ✅ 首选 | ✅ 可以 |
| 联合类型 | ❌ 做不到 | ✅ 核心能力 |
| 映射类型 | ❌ 做不到 | ✅ 核心能力 |
| 条件类型 | ❌ 做不到 | ✅ 核心能力 |
| 声明合并 | ✅ 独占 | ❌ 不支持 |
| 编译性能 | 更快(惰性求值) | 稍慢(即时展开) |
| IDE 错误提示 | 保留类型名,易定位 | 展开成原始类型,可读性差 |
| 扩展第三方 | 声明合并,不侵入 | 需要类型体操 |
| 函数签名 | 啰嗦 | 简洁 |
策略:默认用 interface,需要它做不到的特性时换 type。这是 TypeScript 官方团队推荐的做法。
import type
typescript
// 三个字:加就对了
import type { MyInterface, MyTypeAlias } from './some-module'
// 原因:
// 1. 打破循环依赖(最重要的工程价值)
// 2. 编译后删除,零运行时开销
// 3. 让编译器知道"你不需要在 JS 里找这个东西"
一条贯穿始终的理解
TypeScript 的类型系统有一个根本特征:类型只在编译时存在 。type、interface、import type 都是这个特征的不同表现。接受这个认知,TypeScript 的很多行为你就都能自己推导出来了。