第二章:TypeScript 核心类型系统 - 初级入门
第二章:TypeScript 核心类型系统 - 初级入门
Prorise第二章:TypeScript 核心类型系统 - 初级入门
摘要: 本章将,回归基础,系统性地、一步步地为“前端工程化求索者”构建起完整的 TypeScript 类型知识体系。我们将从最基础的类型注解开始,逐一掌握定义变量、函数、数组和对象所需的所有核心类型工具。从 2.5 节开始,我们会将这些理论知识应用到一个具体的 Todo List 案例中,让您亲眼见证类型系统如何将无序的 JavaScript 代码转化为健壮、可维护的工程化实践。
2.1. 类型化的第一步:类型注解与类型推断
让我们从 TypeScript 最基础、最核心的语法开始:如何为变量赋予类型。
2.1.1. 类型注解
类型注解是 TypeScript 的核心语法,我们使用 (: 类型)
的形式,来 显式地 告诉 TypeScript 一个变量应该是什么类型。
1 | // 我们明确告诉 TypeScript,`appName` 变量的类型必须是 `string` |
2.1.2. 类型推断
很多时候,我们不需要显式地添加类型注解。如果您在声明变量时就进行了初始化,TypeScript 会足够智能,自动 推断 出它的类型。
1 | // 我们没有写 `: number`,但 TypeScript 知道 `version` 是 number 类型 |
2.1.3. 工程实践:何时注解,何时推断?
既然有类型推断,我是不是可以不写类型注解了?
不。记住一个原则:如果一个变量的类型无法在声明时被立刻、清晰地确定,就必须使用类型注解。
比如?
函数参数、函数返回值、未初始化的变量。在这些地方,注解就是你和其他开发者之间最清晰的“契约”。对于像 let i = 0
这样类型一目了然的变量,则可以放心交给类型推断。
2.2. 原子构建块:掌握基础数据类型
2.2.1. 原始类型:string
, number
, boolean
1 | let frameworkName: string = "Vite"; |
2.2.2. 特殊的“空”值:null
与 undefined
1 | let user: null = null; |
在 strictNullChecks
(包含在 strict: true
中) 模式下,null
和 undefined
是独立的类型,不能赋值给 string
或 number
等类型,这从根源上消除了大量的潜在错误。
2.2.3. ES6+ 的新成员:symbol
与 bigint
1 | // symbol 类型的值是唯一的、不可变的 |
2.2.4. 逃生舱与安全阀:any
vs unknown
核心纪律: 在您的工程中,应将 any
视为“最后的手段”。当您不确定一个值的类型时,请优先使用 unknown
,它会强迫您在运行时进行安全的类型检查,这正是 strict
模式所倡导的编程思想。
1 | // any 类型放弃了所有类型检查,应极力避免使用 |
2.2.5. 终点类型:never
never
类型表示一个永远不会正常返回的值的类型。例如,一个总是抛出错误的函数。
1 | function throwError(message: string): never { |
2.3. 为行为建模:函数类型
2.3.1. 函数参数与返回值
1 | // 定义一个函数,它接收两个 number 参数,并返回一个 number |
2.3.2. 可选、默认与 rest 参数
1 | // greeting 是一个带有默认值的参数 |
2.3.3. 函数重载
为一个函数提供多个不同的调用签名,以应对不同的输入情况。
1 | // 为 format 函数提供两个重载签名 |
2.4. 为结构建模 I:数组与元组
2.4.1. 数组 (Array)
1 | // 方式一:类型 [] (推荐,更简洁) |
2.4.2. 元组 (Tuple)
元组是一个 已知长度 和 固定类型顺序 的数组。
1 | // 定义一个元组类型,表示 HTTP 响应 |
2.5. 实战演练:为 Todo List 应用建模
理论学习已经足够,现在是时候将这些“原子构建块”组合起来,解决真实世界的问题了。我们将以第一章创建的 Vite 项目为基础,逐步为一个 Todo List 应用构建类型系统。
2.5.1. 第一步:对象字面量类型
痛点: 我们需要定义应用的状态 state
,它是一个包含 todos
数组和 currentFilter
字符串的对象。在 JS 中,这个对象的结构是模糊的。
解决方案: 使用 对象字面量类型,直接在变量声明时,用 {}
内联地定义对象的形状。
文件路径: src/main.ts
1 | // 清理 main.ts,并写入以下内容 |
这解决了 state
对象本身的结构问题,但 todos
数组内部的对象依然是 any
,我们的核心风险并未解除。内联类型也无法复用。
2.5.2. 第二步:interface
痛点: any[]
无法保证 Todo 项的内部结构一致性。我们需要一个可复用的、专门描述 Todo 项的“契约”。
解决方案: 引入 interface
,为我们的核心数据建模。
文件路径: src/main.ts
1 | import './style.css'; |
思维转变: 通过 interface Todo
,我们完成了从“创建随意对象”到“实例化数据模型”的转变。state.todos
数组现在受到了这份契约的严格保护。
2.5.3. 第三步:type
别名与联合类型
痛点: state.currentFilter
还是一个普通的 string
,我们依然面临“魔术字符串”的风险(例如,误写成 "actives"
)。
解决方案: 引入 type
别名,结合 字面量联合类型,来创建一个精确、有限的状态集合。
文件路径: src/main.ts
1 | // 清理 main.ts,并写入以下内容 |
2.5.4. 第四步:枚举 (enum
)
enum
也可以用来解决“魔术字符串/数字”的问题,它会创建一个真实的 JavaScript 对象。
1 | enum FilterEnum { |
在现代前端开发中,由于 type
创建的字面量联合类型在编译后会被擦除(零运行时开销),且同样能提供完整的类型安全和智能提示,因此它通常是比 enum
更受青睐的选择,所以我们简单了解 enum 即可,他不是最佳实践
2.5.5. 工程选型思辨:interface
vs type
interface
和 type
都能定义对象,我到底该用哪个?
记住这个武断但极其实用的最佳实践:优先使用 interface
来定义对象的形状,只有在 interface
做不到时,才使用 type
。
什么是 interface
做不到的?
就像我们刚刚做的,定义 Filter
联合类型。此外,定义元组、或需要用到交叉、映射等高级类型操作时,都必须用 type
。type
是你的瑞士军刀,但对于定义 API 契约和对象结构,interface
更专注、更清晰。
2.7. 类型断言:当比编译器更懂类型时
在前面的小节中,我们已经学会了如何为应用内部的数据(如 state
、todos
)建立精确的类型契约。然而,在真实的开发场景中,我们经常需要与 TypeScript 类型系统之外的世界进行交互——最典型的例子就是浏览器 DOM。这时,我们就会遇到一个新问题:TypeScript 对这些外部元素的认知是保守的、宽泛的,而我们作为开发者,却拥有更精确的“上下文信息”。
痛点背景:我们需要获取页面上的一个输入框元素,并读取它的 value
值。
第一步:在 index.html
中添加一个输入框
1 | <input type="text" id="todo-input" placeholder="输入新的待办事项..." /> |
第二步:在 main.ts
中尝试获取并操作它
文件路径: src/main.ts
1 | const inputEl = document.querySelector('#todo-input'); |
问题分析: document.querySelector
是一个通用的 DOM API,它不知道我们想获取的是什么具体类型的元素。因此,出于严谨,TypeScript 推断 inputEl
的类型是 Element | null
。通用的 Element
类型上,并不存在 value
这个属性,只有 HTMLInputElement
这样的具体子类型才有。
解决方案: 这时,我们就需要使用 类型断言 。它是一种方式,让我们可以明确地告诉 TypeScript 编译器:“相信我,我知道这个变量的真实类型比你推断的更具体。”
2.7.1. 使用 as
关键字
这是最常用、也是官方推荐的断言语法。
文件路径: src/main.ts
(修正代码)
1 | // 使用 as 关键字进行类型断言 |
as
语法是现代 TypeScript 的首选,因为它在 .tsx
(React/Vue JSX) 文件中不会产生语法冲突。
2.7.2. 遗留的“尖括号”语法
在早期版本的 TypeScript 中,也使用尖括号语法进行断言。
1 | const inputEl = <HTMLInputElement>document.querySelector('#todo-input'); |
最佳实践: 避免使用 尖括号语法。在 React 或 Vue 项目中,尖括号会被解析为 JSX/TSX 标签,从而导致语法混淆和编译错误。请始终坚持使用 as
关键字。
2.7.3. 断言的双面性:信任与风险
核心思维: 类型断言是一把双刃剑。它赋予了我们覆盖编译器类型推断的权力,但同时也意味着 我们将为这个“断言”的真实性负全部责任。如果断言错误,编译器不会再为我们提供保护,错误将直接暴露在运行时。
一个危险的例子:
1 | // 我们的断言是错误的,#todo-input 是一个 input,不是 img |
🤔 思考一下
类型断言和类型转换有什么本质区别?例如,variable as string
和 String(variable)
。
2.7 本章小结
工具 / 语法 | 核心作用 | 说明与场景 |
---|---|---|
interface | 定义对象结构 | 专注于描述对象的“形状”,如 interface Todo { ... } 。 |
type | 创建类型别名 | 更灵活,可为任何类型起别名,如 type AppState = { ... } 。 |
联合类型 | 限定取值范围 | 结合 type 创建联合或字面量类型,如 `type Filter = “all” |
value as Type | (推荐) 强制指定类型 | 开发者向编译器保证类型正确。常用于 DOM 操作或处理 any 类型。 |
<Type>value | (不推荐) 类型断言旧语法 | 功能同 as ,但在 JSX 中会引起语法冲突,应避免使用。 |