第64课:基本类型与类型注解——原始类型、数组、元组、any/unknown/never/void
TypeScript 的核心价值在于静态类型检查:在代码运行之前发现潜在的类型错误。类型注解(Type Annotation)是 TypeScript 的基础语法,它为变量、函数参数和返回值赋予明确的类型约束。本节课将系统讲解 TypeScript 的原始类型、数组与元组声明,以及四个具有特殊语义的顶级类型——any、unknown、never 和 void。理解这些类型的适用场景与限制,是构建类型安全的 TypeScript 程序的第一步。
1. 原始类型(Primitive Types)
TypeScript 的原始类型与 JavaScript 运行时类型一一对应,使用小写形式声明。
1 | let isDone: boolean = true; |
| TypeScript 类型 | 对应 JavaScript 类型 | 示例值 | 说明 |
|---|---|---|---|
boolean |
Boolean | true、false |
逻辑值。 |
number |
Number | 42、3.14、NaN、Infinity |
所有数字(整数、浮点数、特殊值)。 |
string |
String | 'hello'、"world"、模板字符串 |
文本数据。 |
bigint |
BigInt | 100n |
超大整数,目标须 ≥ ES2020。 |
symbol |
Symbol | Symbol('key') |
全局唯一标识。 |
null |
Null | null |
表示空值。 |
undefined |
Undefined | undefined |
表示未定义。 |
1.1 类型推断
TypeScript 能根据初始值自动推断类型,此时不需要显式注解。仅在无法推断(如声明未赋值)或需要明确约束时添加注解。
1 | let inferredString = 'hello'; // 推断为 string |
最佳实践:优先让 TypeScript 推断类型,仅在函数参数、未赋值的变量、复杂对象和需要明确约束的公共 API 上添加类型注解。
2. 数组与元组
2.1 数组类型
声明数组类型有两种等价语法:
1 | let numbers: number[] = [1, 2, 3]; |
推荐使用 T[] 写法,因其更简洁且与 JSX 兼容(<T> 在 .tsx 文件中可能被误认为标签)。
只读数组:使用 readonly 修饰符防止修改。
1 | let readonlyArr: readonly number[] = [1, 2, 3]; |
2.2 元组(Tuple)
元组是一种固定长度、各位置类型已知的数组。适用于表示固定结构的数据(如 CSV 行、坐标对)。
1 | let point: [number, number] = [10, 20]; |
元组的只读版本:readonly [number, number] 拒绝任何修改。
具名元组元素(TypeScript 4.0+):为元组每个位置添加标签,增强可读性。
1 | let httpResponse: [status: number, body: string] = [200, 'OK']; |
2.3 数组与元组的区别
| 特性 | 数组 T[] |
元组 [T1, T2, ...] |
|---|---|---|
| 长度 | 可变 | 固定(声明时确定) |
| 各元素类型 | 统一 | 每个位置类型可不同 |
| 越界访问 | 返回 T 类型 |
返回各位置类型的联合类型 |
| 典型场景 | 同质数据集合 | 异构固定结构(CSV、坐标、API 返回) |
3. 顶级类型:any、unknown、never、void
这四个类型在 TypeScript 类型系统中扮演特殊角色:它们是“容器型”类型,用于表示未知、不可达、无值等极端情况。
3.1 any:逃生舱(禁用检查)
any 类型关闭该变量的所有类型检查,可以赋值为任何值、调用任何方法、赋值给任何类型。它是将 JavaScript 代码快速迁移到 TypeScript 时的“临时方案”,但应尽量避免在生产代码中使用。
1 | let loose: any = 42; |
any 的危险性:它会导致类型污染。any 类型的值在赋值给其他变量时,会“传染”对方。
1 | let dangerous: any = 'could be anything'; |
使用建议:仅在极少数无法确定类型的场景(如解析动态 JSON、迁移遗留代码)使用 any。优先考虑 unknown。
3.2 unknown:类型安全的 any
unknown 表示“我们不知道这个值的类型”。与 any 的区别在于:不能直接对 unknown 值执行任何操作,除非通过类型守卫(Type Guard) 缩窄其类型。
1 | let userInput: unknown = 'hello'; |
| 特性 | any |
unknown |
|---|---|---|
| 赋值给其他类型 | ✅ 允许(破坏类型安全) | ❌ 禁止(需收窄后赋值) |
| 直接调用方法/属性 | ✅ 允许(可能运行时崩溃) | ❌ 禁止(编译时错误) |
| 适用场景 | 快速迁移、动态数据(不推荐) | 安全处理未知外部数据(API 响应、用户输入) |
最佳实践:当确实无法确定类型时,使用 unknown 而非 any,并通过类型守卫安全地使用它。
3.3 void:无返回值
void 表示函数的返回值不应该被使用。它是 undefined 的超类型——undefined 可以赋值给 void,但反之不行。
1 | function logMessage(msg: string): void { |
注意:在 JavaScript 中,无 return 的函数实际返回 undefined。TypeScript 中,void 类型的变量只能接受 undefined(开启 strictNullChecks 时)或 null(关闭时)。
1 | let unusable: void = undefined; // ✅ |
3.4 never:永不存在的值
never 表示永远不会到达或永远不会产生值的类型。通常出现在:
- 永远抛出异常的函数(函数执行中断,无返回值)。
- 死循环函数(函数永不结束)。
- 类型守卫中不可能到达的分支(用于“穷尽性检查”)。
1 | // 抛出异常的函数 |
never 与 void 的区别:void 表示函数没有有意义的返回值(但仍然返回 undefined)。never 表示函数根本没有返回值(抛出异常或永不结束)。never 是所有类型的子类型——它可以赋值给任何类型,但没有任何类型(包括 any)可以赋值给 never。
4. 类型注解与类型推断的平衡
在 TypeScript 中,并不是每个变量都需要显式注解。合理划分推断与注解的边界,既能保持代码简洁,又能确保类型安全。
| 场景 | 推荐做法 | 理由 |
|---|---|---|
| 变量声明时有初始值 | 推断(不加注解) | 类型明确,多余注解增加维护负担。 |
| 函数参数 | 必须注解 | TypeScript 无法推断参数类型,需明确契约。 |
| 函数返回值 | 通常推断 | 简单函数推断准确;复杂函数或公共 API 推荐显式注解以约束意图。 |
| 对象解构、复杂嵌套结构 | 注解或接口 | 明确结构,提升可读性和智能提示。 |
空数组 [] |
必须注解 | 无法推断元素类型,注解如 const arr: string[] = []。 |
课后练习
一、概念自测(选择题 / 填空题)
(单选) 以下关于
unknown类型的描述,正确的是?
A.unknown类型的变量可以调用任意方法。
B.unknown是类型安全的“任意类型”,使用前必须进行类型缩窄。
C.unknown与any行为完全相同。
D.unknown类型变量不能赋值给其他变量。(单选) 一个函数体只有
while (true) {},其返回值类型应该是?
A.void
B.undefined
C.never
D.any(填空) 声明一个包含两个元素的元组,第一个是字符串,第二个是数字,语法为:
let pair: ______ = ['age', 25]。(多选) 以下哪些类型属于 TypeScript 的原始类型?
A.string
B.number
C.object
D.boolean
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个 TypeScript 工具函数 safeGet(obj, path, fallback),它安全地从一个嵌套对象中按路径取值。要求如下:
- 第一个参数
obj为unknown类型(表示不确定的外部数据)。 - 第二个参数
path为点分隔的字符串(如'user.address.city')。 - 第三个参数
fallback为任意类型的默认值。 - 函数内部使用类型守卫逐步缩窄类型(先判断
obj是否为对象,再按路径逐层访问)。 - 如果路径中任何一层不存在或无法访问,返回
fallback值。 - 使用
const声明函数,添加完整的 JSDoc 注释(包括参数类型、返回值和示例)。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 TypeScript 代码。提示词中需明确指定 unknown 的类型缩窄方式、路径解析逻辑和异常处理。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请编写一个 TypeScript 工具函数
safeGet,安全地从嵌套对象中按路径取值。需要创建文件src/safeGet.ts。
- 函数签名:
function safeGet(obj: unknown, path: string, fallback: unknown): unknown。- 先判断
obj是否为对象且非 null(typeof obj === 'object' && obj !== null),否则直接返回fallback。- 将
path按.分割为字符串数组,遍历每个 key:检查当前值是否为对象且包含该 key,若是则继续深入,否则返回fallback。- 遍历结束后返回最终值。
- 添加 JSDoc 注释,包含
@param、@returns和@example。- 在文件末尾添加一个简短的测试代码(使用
console.log验证):
safeGet({ user: { name: 'Alice' } }, 'user.name', 'Unknown')返回'Alice'safeGet(null, 'a.b', 'fallback')返回'fallback'safeGet({}, 'a.b', 'fallback')返回'fallback'- 使用
export导出函数,代码遵循 TypeScript 严格模式。完成后列出完整文件内容。
四、面试真题与参考答案
题目(美团前端面试题):
请详细解释 TypeScript 中
any、unknown和never三个类型的区别,并分别给出一个合适的使用场景。
参考答案:
| 类型 | 含义 | 赋值行为 | 操作行为 | 典型场景 |
|---|---|---|---|---|
any |
任意类型,完全放弃类型检查。 | 可赋值给任何变量,也可接受任何值。 | 可调用任何方法、访问任何属性。 | JavaScript → TypeScript 迁移时的临时过渡(不推荐长期使用)。 |
unknown |
未知类型,类型安全的“任意类型”。 | 只能赋值给 unknown 或 any 类型。 |
不能直接操作,必须通过类型守卫缩窄。 | 处理外部 API 响应、用户输入等不确定的数据。 |
never |
永不存在的值,表示不可达的状态。 | 可赋值给任何类型(底部类型)。 | 没有任何值可以赋给 never。 |
函数中抛出异常或死循环;用于穷尽性检查(switch default)。 |
补充说明:
any是危险的,因为它完全绕过了类型系统,会导致潜在的运行时错误。应该用unknown替代any进行安全的动态数据处理。unknown迫使开发者在操作数据前通过typeof、instanceof或自定义类型守卫验证数据的结构,从而在编译阶段捕获潜在错误。never在类型体操和高级类型编程中扮演关键角色(如条件类型、映射类型过滤),也用于确保所有可能的情况都被处理(穷尽性检查)。
课后练习答案
一、概念自测答案
B
- 解析:
unknown是类型安全的顶层类型,操作前必须缩窄。A 错误(不可直接调用方法);C 错误(行为不同);D 错误(缩窄后可赋值)。
- 解析:
C
- 解析:死循环函数永远不会返回,返回值类型为
never。
- 解析:死循环函数永远不会返回,返回值类型为
[string, number]- 解析:元组语法
[string, number]定义各位置的类型。
- 解析:元组语法
A、B、D
- 解析:
string、number、boolean均为原始类型。object是非原始类型。
- 解析:
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写一个 TypeScript 函数safeGet(obj: unknown, path: string, fallback: unknown): unknown,安全地从嵌套对象中按路径取值。要求:
- 先判断
obj是否为对象且非 null(typeof obj === 'object' && obj !== null),否则直接返回fallback。- 将
path按.分割,使用for...of遍历 key,每次检查current是否为对象且key in current,若是则current = current[key],否则返回fallback。- 遍历结束后返回
current。- 使用
const声明函数,添加 JSDoc 注释(@param、@returns、@example)。- 提供三个测试用例的注释。直接输出完整代码。”