跳到主要内容

第 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 之分
booleantrue / false
symbol唯一标识符Symbol() 的返回值
bigint大整数需目标 ES2020+,写法带 n 后缀
nullnull
undefinedundefined

标注方式与第 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
关于包装类型

stringStringnumberNumber不同的类型:前者是原始类型,后者是包装对象接口。日常标注一律用小写

let s: string // ✅ 原始字符串
let S: String // ❌ 别这么写,它是包装对象,行为不同

两个值得注意的点:

1. null / undefinedstrict 下不是「万能值」

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 / 部分场景 undefinedundefined

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() // 不报错,但可能运行期崩
什么时候可以用 any

不是「永远禁用」,而是有意识地用:比如对接无类型三方库、迁移 JS 代码的过渡期。用的时候尽量用 // eslint-disable 或局部 as any 圈住影响范围,别让它扩散。凡是「这里为什么会变 any」说不清楚的地方,都值得警惕。

2.2.2 unknown:必须先收窄才能用的 any

unknownany 一样能装任何值,区别在于:拿到 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 是类型系统的底类型——任何类型的子集都是它。「一个不可能出现的值,满足任何类型假设」是逻辑自洽的。反过来,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 表示「不关心返回值」,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
数组的 length 是 number

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 里很常用的一招,后面章节会反复出现。

什么时候用 as const
  • 写死一组常量选项,又想让类型精确
  • 把常量对象导出去给别处用,类型不想被放宽
  • 配合 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 仍有价值。但对新代码,默认选字面量联合,遇到具体理由再切回 enum。


本章小结

  • 原始类型用小写;strictnull/undefined 不再是万能值
  • any 关闭检查(黑洞)、unknown 要先收窄、never 表示不可能、void 表示不关心返回值
  • 数组 T[] 关注元素类型;元组 [A, B] 固定长度与位置语义
  • 字面量类型 + 联合类型把取值范围写进类型,string 越少越好
  • as const 冻结对象/数组的字面量;(typeof obj)[keyof typeof obj] 取联合
  • 新代码优先字面量联合,而非 enum

本章练习

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

本章刚学了元组。去 挑战题库 看看 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 产物大小、能否赋非法值、在函数参数里好不好用。写下你的选择依据。