第 2 章 · 基本类型
- 掌握原始类型:
string/number/boolean/null/undefined/symbol/bigint - 彻底分清
any/unknown/never/void四个「特殊类型」的用途与差别 - 理解数组
T[]与元组[A, B]的表示,以及何时用哪个 - 会用字面量类型 + 联合类型精确描述取值范围
- 知道
as const和枚举各自的适用场景
2.1 原始类型
JavaScript 的七种原始类型,TypeScript 里都有对应的类型名:
| 类型 | 值域 | 备注 |
|---|---|---|
string | 任意字符串 | |
number | 任意数字(含 NaN、Infinity) | 没有 int / float 之分 |
boolean | true / false | |
symbol | 唯一标识符 | Symbol() 的返回值 |
bigint | 大整数 | 需目标 ES2020+,写法带 n 后缀 |
null | null | |
undefined | undefined |
标注方式与第 1 章一致:
const name: string = 'ada'
const age: number = 30
const isAdmin: boolean = true
const big: bigint = 9007199254740993n
const unique: symbol = Symbol('id')
const nothing: null = null
const notYet: undefined = undefined
string 和 String、number 和 Number 是不同的类型:前者是原始类型,后者是包装对象接口。日常标注一律用小写:
let s: string // ✅ 原始字符串
let S: String // ❌ 别这么写,它是包装对象,行为不同
两个值得注意的点:
1. null / undefined 在 strict 下不是「万能值」
let x: number = null // strict 下报错:null 不能赋给 number
let y: number | null = null // ✅ 显式说明「可能是 null」
这正是不开 strict 时最容易埋雷的地方——第 12 章会展开。
2. number 只有一个,没有 int / float
let count: number = 3
let ratio: number = 3.14 // 都是 number,TypeScript 不区分整型/浮点
想要「只能是整数」这种约束,得靠字面量联合或模板字面量(2.4、第 8 章)。
2.2 四个特殊类型:any / unknown / never / void
这四兄弟最容易混。先给结论表,再逐个拆:
| 类型 | 一句话 | 能赋给谁 | 谁能赋给它 |
|---|---|---|---|
any | 关闭类型检查的「黑洞」 | 任意类型 | 任意类型 |
unknown | 安全的 any,用前必须收窄 | 只有 any / unknown | 任意类型 |
never | 「不可能的值」 | 任意类型 | 没有任何类型 |
void | 函数没有返回值 | void / 部分场景 undefined | undefined 等 |
2.2.1 any:类型系统的黑洞
any 会把该位置的类型检查整个关闭:
let anything: any = 'hello'
anything = 42 // ✅
anything.foo.bar.baz() // ✅ 编译期全过,运行期爆炸 💥
any 会自动传播:给它赋值,下游变量的类型也变 any。一处 any,一条链的类型保障就没了。
function risky(): any {
return JSON.parse('...') // 返回 any
}
const result = risky() // result 也是 any,后续全部失去检查
result.doAnything() // 不报错,但可能运行期崩
不是「永远禁用」,而是有意识地用:比如对接无类型三方库、迁移 JS 代码的过渡期。用的时候尽量用 // eslint-disable 或局部 as any 圈住影响范围,别让它扩散。凡是「这里为什么会变 any」说不清楚的地方,都值得警惕。
2.2.2 unknown:必须先收窄才能用的 any
unknown 和 any 一样能装任何值,区别在于:拿到 unknown 之后必须证明它是什么,才能使用。
let value: unknown = JSON.parse('{"a":1}')
value.a // ❌ 报错:'value' 的类型为 unknown
value + 1 // ❌ 报错
// 收窄之后才能用
if (typeof value === 'object' && value !== null) {
// 这里 value 收窄为 object
}
unknown 的典型用途:解析外部不可信数据(JSON、接口返回值)。先用 unknown 接住,再逐层校验收窄成可信类型。
function safeParse(input: string): unknown {
return JSON.parse(input)
}
2.2.3 never:表示「不可能」
never 表示永远不可能出现的值。它最常见的两个来源:
1. 抛错或不返回的函数:
function fail(msg: string): never {
throw new Error(msg)
}
function loopForever(): never {
while (true) {}
}
2. 穷尽检查(exhaustiveness)——这是它最实用的场景:
type Status = 'idle' | 'loading' | 'done'
function handle(status: Status) {
switch (status) {
case 'idle': break
case 'loading': break
case 'done': break
default: {
// 这里的 status 是 never
// 如果哪天 Status 加了新值,这里会报错提醒你补分支
const _exhaustive: never = status
}
}
}
never 是类型系统的底类型——任何类型的子集都是它。「一个不可能出现的值,满足任何类型假设」是逻辑自洽的。反过来,never 不能被赋入任何值(因为根本没有这样的值)。
2.2.4 void:函数没有返回值
标注「这个函数不返回任何有意义的值」:
function log(msg: string): void {
console.log(msg) // 不 return,或 return undefined
}
const result: void = log('hi') // result 是 void,什么都不能用
void 表示「不关心返回值」,undefined 是一个具体值。用 void 标注回调、事件处理器(比如 onClick: () => void),比 undefined 更准确——它允许实现方返回任何值而调用方忽略。
四兄弟用一句话总结:
any = 我不想检查了
unknown = 先检查再用
never = 根本不可能
void = 有也不用
2.3 数组与元组
2.3.1 数组:T[] 与 Array<T>
两种写法等价,元素类型必须一致:
const fruits: string[] = ['apple', 'banana']
const nums: Array<number> = [1, 2, 3]
fruits.push('cherry') // ✅
fruits.push(42) // ❌ 42 不是 string
string[] 的 .length 类型是 number。如果你在意「到底几个元素」,那就是元组的活了。
2.3.2 元组:[A, B, C]
元组是长度和每项类型都固定的数组:
// 坐标:第一个是 x,第二个是 y
const point: [number, number] = [10, 20]
// 键值对:字符串 key + 任意 value
const pair: [string, unknown] = ['name', 'ada']
point[0] = 30 // ✅
point[0] = 'x' // ❌ 第 0 位必须是 number
point.push(5) // ⚠️ 元组有时允许 push(长度守卫较弱),慎用
| 场景 | 用数组还是元组 |
|---|---|
| 长度会变、只关心元素类型 | T[] |
| 位置有语义(坐标、范围、键值对) | [A, B] |
| 从函数返回「多个值」 | 元组 [error, data] 或解构 |
| 表示「恰好 N 个」 | 元组 |
从函数返回多个值是元组的高频场景:
function divide(a: number, b: number): [number, number] {
return [Math.floor(a / b), a % b]
}
const [quotient, remainder] = divide(17, 5) // 解构出两个数字
2.3.3 readonly 元组与数组
加 readonly 禁止被修改:
const origin: readonly [number, number] = [0, 0]
origin[0] = 1 // ❌ 只读元组不能改
把数组赋给元组类型不总是安全:
const arr: string[] = ['a', 'b']
const tuple: [string, string] = arr
// ❌ 报错:string[] 长度未知,不能当成恰好两项的元组
元组是「更精确的数组」,反过来不成立。
2.4 字面量类型与联合类型
2.4.1 字面量类型:把「值」变成「类型」
字面量类型就是「只能取某一个值」的类型:
let direction: 'up' // 只能是 'up'
direction = 'up' // ✅
direction = 'down' // ❌ 'down' 不能赋给 'up'
数字、布尔也可以:
const yes: true = true
const answer: 42 = 42
单独的单个字面量用得少,它的威力来自联合。
2.4.2 联合类型:取值范围 = 几个类型的并集
type Direction = 'up' | 'down' | 'left' | 'right'
type Status = 'idle' | 'loading' | 'done'
type Result = 'success' | 'error'
type Mixed = string | number // 字符串或数字
type Maybe = string | null // 或 null
联合类型的变量,取值只能是成员之一:
function move(direction: Direction) {
console.log(`move ${direction}`)
}
move('up') // ✅
move('diag') // ❌ 类型 '"diag"' 不能赋值给 Direction
2.4.3 先认识一下「联合收窄」
拿到联合类型后,用之前要先收窄成具体成员(第 4 章系统讲,这里先感受):
function printId(id: string | number) {
// 此时 id 可能是 string 或 number
if (typeof id === 'string') {
// ✅ 这里 id 收窄为 string
console.log(id.toUpperCase())
} else {
// ✅ 这里 id 收窄为 number
console.log(id.toFixed(2))
}
}
typeof 判断之后,分支内 TS 自动「记住」了更具体的类型——这叫控制流分析,是全书的隐形主角。
2.4.4 字面量联合 vs 普通 string
同样是「取值受限」,两者差别巨大:
// 1. 普通 string —— 任何字符串都行,容易传错
function setColor(c: string) {}
// 2. 字面量联合 —— 只有这几个值合法,传错立刻报错
type Theme = 'light' | 'dark' | 'system'
function setTheme(t: Theme) {}
setTheme('blue') // ❌ 编译期拦下
能用字面量联合就别用 string——它把「取值范围」写进了类型,是第 1 章「好类型要精确」的直接体现。
2.5 as const:让对象/数组的字面量「冻住」
一个常见困惑:明明写的是 'dark',类型却是 string:
const theme = { mode: 'dark' }
theme.mode // 类型是 string,不是 'dark'
// 为什么?因为对象属性默认「可变、可扩展」,TS 推断成宽类型 string
用 as const 把字面量冻结为精确类型:
const theme = { mode: 'dark' } as const
theme.mode // 类型是 'dark'(字面量)
as const 对数组同样生效,还能和联合类型配合:
const directions = ['up', 'down', 'left', 'right'] as const
// directions 的类型是 readonly ['up', 'down', 'left', 'right']
type Direction = (typeof directions)[number] // 借助 typeof 取出联合
// 等价于 'up' | 'down' | 'left' | 'right'
(typeof directions)[number] 是啥typeof directions 拿到数组的类型,[number] 表示「每一项的类型」——于是得到一个联合类型。这是 TS 里很常用的一招,后面章节会反复出现。
- 写死一组常量选项,又想让类型精确
- 把常量对象导出去给别处用,类型不想被放宽
- 配合
satisfies(第 3 章)校验同时保持字面量
2.6 枚举 enum vs 字面量联合
2.6.1 enum 是什么
enum 是 TS 特有的「命名常量集合」语法:
enum Direction {
Up,
Down,
Left,
Right,
}
Direction.Up // 0(数字枚举默认从 0 递增)
Direction[0] // 'Up'(数字枚举有反向映射)
也可以显式赋值,或用字符串枚举:
enum Status {
Idle = 'idle',
Loading = 'loading',
Done = 'done',
}
2.6.2 enum 的三个问题
1. 运行时有额外代码。 enum 会编译成真实的对象,而不是被擦掉:
// enum Direction { Up, Down } 编译后是:
var Direction
;(function (Direction) {
Direction[(Direction['Up'] = 0)] = 'Up'
Direction[(Direction['Down'] = 1)] = 'Down'
})(Direction || (Direction = {}))
2. 数字枚举 + 任意数字。 数字枚举成员会被赋给任意数字,失去了约束力:
let d: Direction = 999 // ✅ 编译期不报错!数字枚举防不住
3. 与「结构类型」的世界格格不入。 后面会学到 TS 是结构类型系统,而 enum 是「名义类型」——它俩的组合有时很别扭。
2.6.3 推荐:字面量联合 + 常量对象
现代 TS 社区的主流推荐:
const Direction = {
Up: 'up',
Down: 'down',
Left: 'left',
Right: 'right',
} as const
type Direction = (typeof Direction)[keyof typeof Direction]
// 等价于 'up' | 'down' | 'left' | 'right'
既拿到精确的类型(只接受这四个字符串),又拿到运行时对象(Direction.Up),还没有 enum 的那些毛病。
| 维度 | enum | 字面量联合 + as const |
|---|---|---|
| 编译产物 | 生成额外 JS 对象 | 无(类型被擦掉) |
| 类型约束 | 数字枚举可被任意数字绕过 | 严格 |
| 与第三方库 | 结构类型兼容性好 | 更好 |
| 反向映射 / 遍历 | 数字枚举有 | 需要自己写 |
遗留代码、或确实需要「运行时按名字/值双向查找」且数量不多时,enum 仍有价值。但对新代码,默认选字面量联合,遇到具体理由再切回 enum。
本章小结
- 原始类型用小写;
strict下null/undefined不再是万能值 any关闭检查(黑洞)、unknown要先收窄、never表示不可能、void表示不关心返回值- 数组
T[]关注元素类型;元组[A, B]固定长度与位置语义 - 字面量类型 + 联合类型把取值范围写进类型,
string越少越好 as const冻结对象/数组的字面量;(typeof obj)[keyof typeof obj]取联合- 新代码优先字面量联合,而非
enum
本章练习
本章刚学了元组。去 挑战题库 看看 00018「获取元组长度」的题意,先想想法子,等第 5 章学会泛型后回来动手——你会发现它突然变得很简单。
练习 1 · 特殊类型辨析
分别写出一段用 any / unknown / never / void 的代码,并说明为什么用这个类型合适、换了别的会出什么问题。
练习 2 · 坐标元组
声明一个表示二维坐标的元组类型,写一个函数接收坐标、返回「离原点的距离」,并正确标注入参出参:
// 提示:坐标 (x, y),距离 = Math.hypot(x, y)
type Point = [number, number]
function distance(p: Point): number { /* ... */ }
练习 3 · 状态机字面量联合
用字面量联合定义订单状态 'pending' | 'paid' | 'shipped' | 'done',写一个函数根据状态返回不同的提示文案(用 switch + 穷尽检查 never)。
练习 4 · as const 冻结配置
定义一个 config 对象(含 mode: 'dark'、lang: 'zh'),先用普通写法观察推断出的类型,再用 as const 观察类型变化,最后用 (typeof config)[keyof typeof config] 取出 'dark' | 'zh'。
练习 5 · enum vs 字面量联合
用两种方式分别定义「颜色主题」'red' | 'green' | 'blue':enum 版和字面量联合版。对比:tsc 编译后的 JS 产物大小、能否赋非法值、在函数参数里好不好用。写下你的选择依据。