跳到主要内容

第 3 章 · 接口与类型别名

读完本章你会
  • interface 描述对象结构:必选属性、可选属性、只读属性
  • 用索引签名约束「键不固定」的对象
  • 理解接口继承 extends 与「重复声明合并」这两个 interface 的独门特性
  • 分清 interfacetype 的适用场景,知道该优先用哪个
  • 掌握类型断言 as 与非空断言 ! 的正确用法与风险

3.1 interface 是什么

描述「一个对象长什么样」,最常用的是 interface

interface User {
name: string
age: number
email: string
}

它既定义了类型,又声明了这个对象必须包含哪些字段

const ada: User = { name: 'ada', age: 30, email: 'ada@example.com' } // ✅

const bob: User = { name: 'bob', age: 28 } // ❌ 缺少 email
const carol: User = { name: 'carol', age: 25, email: 'c@x.com', admin: true } // ❌ 多了 admin
结构类型:核心心智

TS 判断「能不能赋值」靠结构,不靠名字。只要字段对得上,interface 之间、interface 和对象字面量之间都可以互相满足。这跟 Java/C# 那种「必须实现同一个接口」完全不同。

对象字面量有个额外规则——多余属性检查(excess property check):直接赋给字面量时,多了没声明的属性会报错(上面的 carol 例子)。但如果先放进变量再赋值,这条检查会放宽:

const extra = { name: 'carol', age: 25, email: 'c@x.com', admin: true }
const dave: User = extra // ✅ 不报错(字段齐全,多余属性被容忍)

这个「变量 vs 字面量」的差别常让新手困惑:多余属性检查只对字面量直接赋值生效


3.2 可选属性与只读属性

3.2.1 可选属性 ?

? 表示「可以没有」:

interface Profile {
name: string
nickname?: string // 可选
}

可选属性给出去后是 string | undefined 的效果:

function greet(p: Profile) {
console.log(p.name)
console.log(p.nickname.toUpperCase()) // ❌ 'nickname' 可能为 undefined
console.log(p.nickname?.toUpperCase()) // ✅ 可选链
}
可选 ≠ 一定为 undefined
const p1: Profile = { name: 'ada' } // ✅ nickname 直接没有
const p2: Profile = { name: 'ada', nickname: undefined } // ✅ 也可以显式给 undefined

可选属性在类型层面等价于 string | undefined,但对象的键是否存在取决于你。访问时一律按「可能为 undefined」处理。

3.2.2 只读属性 readonly

readonly 表示「初始化后不能再改」:

interface Config {
readonly apiKey: string
readonly debug: boolean
}

const cfg: Config = { apiKey: 'sk-xxx', debug: false }
cfg.apiKey = 'other' // ❌ 只读属性不能赋值

readonly编译期检查,不影响运行时——编译成 JS 后只是普通属性。它防的是「代码里不该改的地方乱改」,不是「运行时真的冻结」。


3.3 索引签名:描述「键不固定」的对象

当对象的键事先不确定、但值的类型一致时,用索引签名:

// 一个「字符串 → 数字」的字典
interface ScoreMap {
[name: string]: number
}

const scores: ScoreMap = { ada: 95, bob: 88 }
scores['carol'] = 90 // ✅ 任意字符串键都可以

两种索引签名:

写法含义例子
[key: string]: T字符串键字典、配置表
[key: number]: T数字键数组/类数组
索引签名会「覆盖」具体属性的约束

索引签名要求所有属性的值的类型都兼容它:

interface Dict {
[key: string]: string
// 如果想加一个 number 属性,会报错:
// age: number // ❌ 属性 'age' 的类型与索引签名不兼容
}

想让「大部分任意 + 个别特定」,需要用联合:[key: string]: string | number

索引签名的典型用途——配置表 / 字典 / 枚举映射

const errorMessages: Record<string, string> = {
required: '此项必填',
length: '长度不合法',
}

Record<K, V> 是内置工具类型(第 6 章手写它),等于「键是 K、值是 V 的对象」。


3.4 接口继承与声明合并

这两个特性是 interface 独有、type 没有的。

3.4.1 继承 extends

一个接口可以继承另一个,复用字段:

interface Animal {
name: string
age: number
}

interface Dog extends Animal {
breed: string // 在 Animal 基础上多一个字段
}

const d: Dog = { name: '旺财', age: 3, breed: '柯基' }

可以多继承:

interface Named { name: string }
interface Aged { age: number }
interface Person extends Named, Aged {}

// Person = { name: string; age: number }
继承 vs 组合

继承(extends)表示「is-a」:Dog 是一个 Animal。组合(字段里包含另一个对象)表示「has-a」:Car 有一个 Engine。优先用组合——它更灵活,避免继承层级过深。只有当「确实要求一个类型必须包含另一个的全部字段」时,继承才顺手。

3.4.2 声明合并(declaration merging)

同名 interface 会自动合并字段:

interface Window {
title: string
}
interface Window {
version: string
}
// 合并后:interface Window { title: string; version: string }

这个特性在「给第三方库的类型打补丁」时很有用(第 9 章会用到 declare global 配合它)。


3.5 interface vs type:怎么选

对象类型也可以用类型别名 type 写:

type User = {
name: string
age: number
}

两者在描述对象时几乎等价,区别在于几个边界能力:

能力interfacetype
描述对象
继承 / 合并extends、同名合并❌ 不支持
联合 / 交叉 / 元组 / 原始类型别名✅ 都能干
映射 / 条件类型(第 6-7 章)
// type 能做的、interface 做不到的:
type Status = 'idle' | 'loading' // 联合
type Point = [number, number] // 元组
type ID = string // 别名
type Maybe<T> = T | null // 泛型别名
社区共识

描述对象优先 interface(能继承、能合并);需要联合/元组/别名/泛型运算时用 type。两者混用没问题——只要在一个项目里保持一致。别被「必须二选一」的焦虑绑架,TS 官方态度是「按场景挑顺手的」。

一个常见练习——什么时候 type 更合适:

// 一个「要么是加载中,要么是成功数据」的状态
type AsyncState<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
// 这是可辨识联合(第 4 章主角),只能用 type 表示

3.6 类型断言:as!

3.6.1 类型断言 as

当你比 TS 更清楚一个值的真实类型时,用 as 告诉它「听我的」:

// 从 DOM 取元素,TS 只知道是 HTMLElement
const input = document.querySelector('input') as HTMLInputElement
input.value // 现在 TS 认为是 HTMLInputElement
断言的代价

as绕过类型检查的开关。断言错误 = 运行时炸。它只在「你确实比 TS 知道得更多」时使用:

const n = 42
n as string // ⚠️ 不报错但纯属胡来(number 断言成 string)
JSON.parse(x) as User // 合理:外部数据,你校验过结构

两个常见变体:

  • as const:把字面量冻结为精确类型(第 2 章)
  • as unknown as T:跨越不相容类型时的「双断言」(比如把复杂结构强制转型),慎用,通常是设计有问题

3.6.2 非空断言 !

! 表示「这里我保证不为空」:

const el = document.getElementById('app')!
el.style.display = 'block' // 断言 el 不是 null,跳过判空
! 不是运行时保护

! 只是骗过编译器,如果运行时真的为 null,照样崩:

const el = document.getElementById('不存在的id')!
el.click() // 💥 运行时报错:el 是 null

能用收窄(第 4 章)就优先收窄,! 留给「确定存在」的场景。


本章小结

  • interface 描述对象:必选 / 可选 ? / 只读 readonly
  • 索引签名处理「键不固定」的对象;注意它会对具体属性值类型产生约束
  • extends 继承与同名合并是 interface 独有能力
  • 对象用 interface,联合/元组/别名/泛型运算用 type
  • as 断言和 ! 非空断言是绕过检查的「快捷键」,用对场景、控制影响面

本章练习

前置挑战(学完第 6 章后回来做)

挑战题库 看看 00004「实现 Pick」的题意——「从类型 T 中选出符合 K 的属性构造新类型」。这需要第 6 章的映射类型,先记着,到时回来秒解。

练习 1 · 继承 vs 组合

分别用 extends 继承和「字段内嵌对象」两种方式建模「管理员用户」(普通用户 + rolepermissions),对比两种写法的可扩展性,写下你的选择理由。

练习 2 · 配置对象

用索引签名 + readonly 定义一个「环境配置」:键为字符串,值为 string | number | boolean,且整体只读。给它赋几组值验证。

练习 3 · interface vs type 决策清单

列出你在什么场景会选 interface、什么场景会选 type,每条配一个真实例子。至少各两条。

练习 4 · 可选属性与 strict

定义 interface Option { label?: string; value: number },在 strict: true 下访问 option.label,观察报错;用可选链 ?. 和「先判空」两种方式修复,体会差别。

练习 5 · 安全地断言

对接一个返回 unknown 的解析函数,写一个类型守卫风格的校验函数,只有在 typeof / 字段检查都通过后才 as User,避免无脑断言:

function parseUser(raw: unknown): User | null {
// 用 typeof + in + 字段检查,验证通过才 as User
}