Skip to content
接口与类型别名

接口与类型别名

本章涵盖 TypeScript 类型系统的两大核心抽象工具:interface(接口)和 type(类型别名)。两者均能描述对象形状,但在扩展方式、声明合并、适用场景上有本质区别。本章从零讲清接口全部语法,再与类型别名逐项对比,最后给出选型决策表。


interface 接口

interface 是 TypeScript 的核心抽象机制,为对象、函数、类的形状命名。它定义了一份"契约"——实现该接口的值必须满足其结构约束。

🔬 深入原理:TypeScript 采用结构类型(Structural Typing),而非名义类型。只要值的形状满足接口定义,就视为实现了该接口,无需显式 implements 声明。这被称为"鸭子类型"(duck typing)。

基本语法

interface User {
    name: string
    age: number
}

const user: User = { name: "Willow", age: 18 }

分号或逗号均可作为分隔符,也可省略(换行分隔)。以下写法等价:

interface User {
    name: string
    age: number
}

// 也合法,但风格不统一,建议选一种并在项目中保持一致
interface User {
    name: string;
    age: number;
}

可选属性

在属性名后加 ? 表示可选,赋值时可省略该属性:

interface Config {
    url: string
    method?: string       // 可选
    timeout?: number      // 可选
}

const c1: Config = { url: "/api" }                     // ✅
const c2: Config = { url: "/api", timeout: 5000 }      // ✅

💡 最佳实践:可选属性与索引签名搭配时要小心——当索引签名的值类型与可选属性不一致时可能引发类型冲突。详见"常见陷阱"。

只读属性

readonly 修饰的属性只能在对象初始化时赋值,之后无法修改:

interface Point {
    readonly x: number
    readonly y: number
}

const p: Point = { x: 10, y: 20 }
// p.x = 30  // ❌ Cannot assign to 'x' because it is a read-only property.

🚨 陷阱readonly 仅约束第一层赋值,不提供深层不可变性。对于嵌套对象,内层属性仍可修改:

interface Config {
    readonly db: { host: string; port: number }
}
const c: Config = { db: { host: "localhost", port: 5432 } }
c.db.host = "remote"  // ✅ 允许!readonly 不递归

需要深度只读时,使用 Readonly<T> 映射类型或 as const 断言。

ReadonlyArray<T> 是内置的只读数组类型,可防止 push/splice 等变异操作:

const arr: ReadonlyArray<number> = [1, 2, 3]
// arr.push(4)  // ❌ Property 'push' does not exist on type 'readonly number[]'

索引签名

当对象的键名不确定时,使用索引签名描述所有键值对的类型:

// 所有 key 都是 string,所有 value 也是 string
interface StringMap {
    [key: string]: string
}

const m: StringMap = {
    name: "Willow",
    city: "Hangzhou"
}

索引签名的 key 类型只能是 stringnumber(或模板字面量类型,TS 4.4+):

// 数字索引 —— 数组的底层定义
interface NumberArray {
    [index: number]: string
}

const arr: NumberArray = ["a", "b", "c"]
console.log(arr[0])  // "a"

// 模板字面量索引(TS 4.4+)
interface EventMap {
    [key: `on${Capitalize<string>}`]: (value: unknown) => void
}

const handlers: EventMap = {
    onClick: (v) => {},   // ✅
    onChange: (v) => {},  // ✅
    // submit: (v) => {}  // ❌ key 必须以 "on" 开头并大写第二个字母
}

🚨 陷阱:同时声明字符串索引和明确属性时,所有明确属性的类型必须是索引签名值类型的子类型:

interface Bad {
    [key: string]: number
    name: string  // ❌ Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}

函数类型接口

interface 可直接描述一个函数的调用签名:

interface Comparator {
    (a: number, b: number): number
}

const compare: Comparator = (a, b) => a - b

也可以同时描述函数的属性和调用签名,构成混合类型

interface Counter {
    (): void                  // 可调用
    count: number             // 有属性
    reset(): void             // 有方法
}

function createCounter(): Counter {
    const counter = (() => { counter.count++ }) as Counter
    counter.count = 0
    counter.reset = () => { counter.count = 0 }
    return counter
}

const c = createCounter()
c()               // count = 1
c()
console.log(c.count)  // 2
c.reset()
console.log(c.count)  // 0

🔬 深入原理:混合类型描述了 JavaScript 中常见的"可调用对象"模式(jQuery 的 $、lodash 的 _ 都是既可作为函数调用又带有方法的对象)。使用 interface 描述比 type 更自然,且可利用声明合并追加属性。

接口继承(extends)

extends 关键字让接口继承一个或多个接口的成员,实现类型复用:

interface Animal {
    name: string
}

interface Dog extends Animal {
    breed: string
}

const dog: Dog = { name: "Buddy", breed: "Golden Retriever" }

多重继承——一个接口可以同时继承多个父接口:

interface Animal {
    name: string
}

interface Pet {
    owner: string
}

interface Cat extends Animal, Pet {
    purr(): void
}

const cat: Cat = {
    name: "Whiskers",
    owner: "Willow",
    purr: () => console.log("purr...")
}

子接口可以覆写父接口的成员,但必须兼容父类型(覆写后的类型必须是原类型的子类型):

interface Base {
    id: string | number
}

interface Derived extends Base {
    id: number  // ✅ number 是 string | number 的子类型
}

声明合并(Declaration Merging)

这是 interface 独有的特性——多个同名 interface 声明会自动合并,不会报错:

interface Window {
    title: string
}

interface Window {
    version: number
}

// 自动合并为:
// interface Window {
//     title: string
//     version: number
// }

const win: Window = { title: "My App", version: 1 }  // ✅

🔬 深入原理:声明合并在编译期完成,TypeScript 收集所有同名接口的成员,合并为一个接口类型。这一机制使得 TypeScript 可以向已有类型"增量添加"属性,是扩展第三方库类型的核心手段。type 不支持此特性——同名 type 会直接报错。

典型应用——扩展第三方库的类型定义:

// 扩展 Express 的 Request 对象
declare namespace Express {
    interface Request {
        user?: { id: string; role: string }
    }
}

// 扩展全局类型
interface String {
    isEmail(): boolean
}

🚨 陷阱:声明合并是全局性的。在 .d.ts 文件中修改全局接口会影响整个项目。应谨慎使用,避免污染全局命名空间或在无意中覆盖已有属性。


type 类型别名

type 为任意类型赋予别名——不仅限于对象,还涵盖原始类型、联合类型、元组、函数签名等所有 TypeScript 类型。

基本别名

type Name = string
type Age = number
type Point = [number, number]

const name: Name = "Willow"
const age: Age = 18
const pos: Point = [120, 30]

对象类型别名

type User = {
    name: string
    age: number
    address?: string
}

const user: User = { name: "Willow", age: 18 }

联合类型(Union)

type 的核心优势——定义一组可选值的联合:

type Status = "pending" | "success" | "error"
type ID = string | number

function handle(s: Status) { /* ... */ }
handle("pending")   // ✅
// handle("done")   // ❌ Argument of type '"done"' is not assignable

type Result<T> = { ok: true; value: T } | { ok: false; error: string }

交叉类型(Intersection)

& 运算符合并多个类型,要求值同时满足所有约束:

type Nameable = { name: string }
type Aged = { age: number }
type Person = Nameable & Aged

const p: Person = { name: "Willow", age: 18 }  // ✅

🚨 陷阱:交叉类型合并同名属性时,类型会取交集。若交集为空(如 { x: string } & { x: number }),属性类型变为 never,导致无法赋值:

type A = { x: string }
type B = { x: number }
type C = A & B
// C = { x: never }  ← string 和 number 无交集

元组与函数别名

type Point2D = [number, number]
type Point3D = [number, number, number]

type Callback = (data: string) => void
type Handler<T> = (event: T) => void
type Constructor<T> = new (...args: any[]) => T

配合映射类型和条件类型

type 在此类高级类型中不可或缺(interface 无法直接使用映射类型):

type ReadonlyUser = Readonly<User>
type PartialUser = Partial<User>
type UserKeys = keyof User                      // "name" | "age" | "address"

type Nullable<T> = { [K in keyof T]: T[K] | null }
type IsString<T> = T extends string ? "yes" : "no"

interface vs type 对比

特性 interface type
描述对象形状
描述函数类型
联合类型 (|)
交叉类型 (&) 通过 extends 间接实现 ✅ 直接使用 &
元组类型 ❌(可用索引签名模拟)
原始类型别名
声明合并 ✅(同名自动合并) ❌(报错 “Duplicate identifier”)
扩展方式 extends(可继承多个) & 交叉运算符
IDE 悬停提示 显示接口名称 展开所有成员(或显示名称,取决于复杂度)
extends 从句 ❌(仅可用在泛型约束中)
implements 从句
映射类型
条件类型
同名冲突 自动合并(安全) 编译错误
递归引用 ✅ 支持自引用 ✅ 支持自引用

性能提示

性能提示interface 的 extends 检查采用"预计算+缓存"策略,在大型代码库中编译速度通常优于深层嵌套的 & 交叉类型。type 使用交叉合并大量类型时,编译器需要展开每一个成员,可能导致类型检查变慢。对于简单的对象形状,两者性能差异可忽略不计。

IDE 体验差异

interface User {
    name: string
    age: number
}

type Person = {
    name: string
    age: number
}

// 悬停在 User 上显示: interface User
// 悬停在 Person 上显示: type Person = { name: string; age: number; }

interface 名称常在错误提示中直接展示,可读性更好;type 展开成员虽然信息更多,但复杂类型可能让错误信息冗长。


选型建议

💡 最佳实践

  • 优先使用 interface 描述对象 / 类的公共 API(可扩展、IDE 友好、编译快)
  • 使用 type 定义联合类型、元组、映射类型、条件类型
  • 使用 type 定义复杂的函数签名(配合泛型时表达能力更强)
  • 给第三方库"打补丁"时,利用 interface 声明合并
  • 同一个概念只声明一次——不要同时用 interfacetype 描述同一个形状
  • 团队中协商并文档化规则,保持代码库内一致

选型决策树:

需要联合/元组/映射/条件类型?
├── 是 → type
└── 否 → 需要声明合并或 extends 继承?
         ├── 是 → interface
         └── 否 → 描述对象/类 API?
                  ├── 是 → interface(优先)
                  └── 否 → type

常见陷阱汇总

# 陷阱 说明 解决
1 误以为 type 支持声明合并 同名 type 直接报错,不会合并 interface,或将多个 type 合并为交叉类型
2 多余属性检查(Excess Property Checking) 字面量赋值给接口时,多余的属性会报错 使用中间变量、索引签名、或 as 类型断言
3 interface 同名合并污染全局 .d.ts 中不经意的同名接口会全局合并 使用 namespace 包裹,或在模块内声明(非全局脚本文件)
4 索引签名吞没所有键 [key: string]: T 要求所有已知属性值类型是 T 的子类型 将索引签名的值类型设为足够宽泛的类型(如 unknown
5 交叉类型属性冲突 { x: string } & { x: number } 导致 x: never,无法构造 避免同名属性冲突,或使用 Omit 重命名后交叉
6 循环引用导致类型展开死循环 自引用类型嵌套过深时编译报错 将递归部分包装在对象属性中(如 type Tree = { value: T; children: Tree[] }

多余属性检查示例

interface User {
    name: string
    age: number
}

// 字面量直接赋值 —— 报错
// const u: User = { name: "Willow", age: 18, email: "test@test.com" }
// ❌ 'email' does not exist in type 'User'

// 中间变量赋值 —— 不报错
const obj = { name: "Willow", age: 18, email: "test@test.com" }
const u: User = obj  // ✅ 通过(结构性类型检查)

🔬 深入原理:多余属性检查是 TypeScript 的特殊规则,仅在字面量直接赋值给明确类型注解的变量时触发。这是一个实用性设计,旨在捕获拼写错误的属性名。通过中间变量赋值时会绕过此检查,因为此时 TS 只做结构性类型检查。


延伸阅读