TypeScript 类型系统:`type` vs `interface` 与 `import type`

前言

TypeScript 中最容易让初学者困惑的两个问题:typeinterface 到底有什么区别?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') }
}

结论:在"描述对象结构"这件事上,typeinterface 能力几乎完全重叠。

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 typeexport 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 typeinterface、泛型标注 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 的类型系统有一个根本特征:类型只在编译时存在typeinterfaceimport type 都是这个特征的不同表现。接受这个认知,TypeScript 的很多行为你就都能自己推导出来了。


相关推荐
晓说前端17 小时前
TypeScript 高级特性 —— 类型断言与泛型
前端·typescript
烬羽1 天前
React 状态提升:一个 ColorPicker,彻底搞懂状态该放哪里
react.js·typescript·设计
ji_shuke2 天前
Playwright 从零到 CI 实战:基于 TypeScript 的端到端测试完整指南
javascript·ci/cd·typescript·playwright
小林ixn2 天前
React + TypeScript 实战:从“类型体操”到“数据持久化”,一次讲透组件通信与副作用管理
前端·react.js·typescript
用户0934077735142 天前
HarmonyOS WPS Open SDK 快速入门:HAR 装上就能打开文档吗
typescript·harmonyos
不好听6132 天前
从困惑到理解:React 父子组件通信的三种写法
react.js·typescript
烬羽2 天前
React 状态归属:两个版本的用户名编辑器,告诉你 state 该放哪
react.js·typescript·前端框架
breeze jiang2 天前
React + TypeScript 编辑表单:为什么要区分 name 和 editingName
前端·typescript
gis开发之家3 天前
《Vue3 从入门到大神40篇》Vue3 源码详解(十):diff 算法全解析 —— 为什么 Vue3 比 Vue2 更快?
javascript·算法·typescript·前端框架·vue3·vue3源码