第 1 节 概述
前几篇我们学会了用类型标注和泛型来描述数据。但代码运行时,类型信息被擦除了——运行时没有类型,只有值 。
怎么确保在运行时安全地使用联合类型中的某一分支?怎么告诉 TypeScript “这个变量现在就应该是这个类型”?
类型守卫(Type Narrowing) 就是干这个的。它让 TypeScript 在特定代码分支中,将一个宽类型缩小为一个窄类型。
第 2 节 typeof 守卫
typeof 操作符可以在运行时检查值的基本类型,TypeScript 能理解这个检查:
1
2
3
4
5
6
7
8
9
10
11
function format ( value : string | number ) {
if ( typeof value === "string" ) {
// 这里 value 的类型被缩小为 string
return value . toUpperCase ()
}
// 这里 value 的类型被缩小为 number
return value . toFixed ( 2 )
}
format ( "hello" ) // "HELLO"
format ( 3.1415 ) // "3.14"
typeof 能识别的类型有:string、number、bigint、boolean、symbol、undefined、object、function。
1
2
3
4
5
6
7
function log ( value : unknown ) {
if ( typeof value === "function" ) {
value () // ✅ function 类型
} else if ( typeof value === "object" && value !== null ) {
console . log ( value ) // ✅ object 类型(非 null)
}
}
第 3 节 真值守卫
JavaScript 中,null、undefined、0、""、false、NaN 都是假值。利用真假值检查可以缩小类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getLength ( value? : string | null ) {
// value 可能是 string | undefined | null
if ( value ) {
// 这里 value 被缩小为 string(假值被排除)
return value . length
}
return 0
}
function printAll ( values? : string [] | null ) {
if ( values && values . length > 0 ) {
// 先排除 null/undefined,再取值
for ( const v of values ) {
console . log ( v )
}
}
}
注意
真值守卫会排除 0 和 ""。如果这些值是合法的,不要用真值守卫:
1
2
3
4
5
function process ( score : number | null ) {
if ( score ) { // ❌ 0 是合法的分数,但不能通过守卫
// ...
}
}
第 4 节 等值守卫
用 ===、!== 比较也能缩小类型范围:
1
2
3
4
5
6
7
8
9
10
function move ( direction : "up" | "down" | "left" | "right" | null ) {
if ( direction === null ) {
return // direction 在这里是 null
}
if ( direction === "up" ) {
// direction 在这里是 "up"
}
// 剩下的分支:"down" | "left" | "right"
}
第 5 节 in 操作符
JavaScript 的 in 操作符检查对象是否有某个属性。TypeScript 能用它区分联合类型的成员:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Fish {
swim () : void
finCount : number
}
interface Bird {
fly () : void
wingSpan : number
}
type Pet = Fish | Bird
function handlePet ( pet : Pet ) {
if ( "swim" in pet ) {
// 这里 pet 被缩小为 Fish
console . log ( `鱼有 ${ pet . finCount } 个鳍` )
} else {
// 这里 pet 被缩小为 Bird
console . log ( `鸟的翼展是 ${ pet . wingSpan } 米` )
}
}
第 6 节 instanceof 守卫
instanceof 用于检查对象是否属于某个类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dog {
bark() { console . log ( "汪汪" ) }
}
class Cat {
meow() { console . log ( "喵喵" ) }
}
type Pet = Dog | Cat
function makeSound ( pet : Pet ) {
if ( pet instanceof Dog ) {
pet . bark () // ✅ Dog 类型
} else {
pet . meow () // ✅ Cat 类型
}
}
第 7 节 可辨识联合
可辨识联合是最强大的类型守卫模式。它的核心是:联合类型的每个成员都有一个相同的标识属性 (通常是 type 或 kind):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Circle = {
kind : "circle"
radius : number
}
type Rectangle = {
kind : "rectangle"
width : number
height : number
}
type Triangle = {
kind : "triangle"
base : number
height : number
}
type Shape = Circle | Rectangle | Triangle
function area ( shape : Shape ) : number {
switch ( shape . kind ) {
case "circle" :
return Math . PI * shape . radius ** 2
case "rectangle" :
return shape . width * shape . height
case "triangle" :
return ( shape . base * shape . height ) / 2
}
}
每个 case 分支中,TypeScript 都知道对应的具体类型,可以安全地访问专属属性。
技巧
可辨识联合是 TypeScript 中最常用也最优雅的模式之一。后端 API 响应、状态管理、AST 节点等场景都会用到。用字符串字面量作为标识属性,可以同时享受自动补全和类型安全。
第 8 节 类型断言
当你知道某个值的类型比 TypeScript 推断的更精确时,可以用类型断言告诉编译器:“相信我,我知道我在做什么。”
1
2
3
4
5
6
7
// 获取 DOM 元素
const canvas = document . getElementById ( "canvas" ) as HTMLCanvasElement
// canvas 的类型被断言为 HTMLCanvasElement,而不是 HTMLElement | null
// 从宽类型断言为窄类型
const value : unknown = "hello"
const length = ( value as string ). length
两种断言语法等价:
1
2
const a = value as string // JSX 中推荐
const b = < string > value // JSX 中与标签冲突,不推荐
非空断言——告诉 TypeScript 这个值不是 null 或 undefined:
1
2
3
function process ( id? : string ) {
console . log ( id ! . toUpperCase ()) // 告诉编译器 id 一定存在
}
危险
类型断言绕过了类型检查,使用要克制。先试试类型守卫,实在不行再用断言。特别是 as any——几乎永远不需要。
第 9 节 satisfies 操作符
satisfies 是 TypeScript 4.9 引入的运算符。它检查变量的类型是否符合某个类型,但保留变量的精确推断类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Color = "red" | "green" | "blue"
type Palette = Record < string , Color >
const palette = {
primary : "blue" ,
secondary : "green" ,
danger : "red" ,
} satisfies Palette
// palette.primary 的类型是 "blue"(字面量),而不是 Color
// 但 palette 整体满足 Palette 的约束
const wrongPalette = {
primary : "blue" ,
secondary : "yellow" , // ❌ "yellow" 不在 Color 中
} satisfies Palette
对比一下传统写法:
1
2
3
4
5
6
7
// 方式一:直接注解 —— 失去了精确的字面量类型
const p1 : Palette = { primary : "blue" , secondary : "green" }
p1 . primary // 类型为 Color("red" | "green" | "blue"),不是 "blue"
// 方式二:satisfies —— 既验证,又保留精确类型
const p2 = { primary : "blue" , secondary : "green" } satisfies Palette
p2 . primary // 类型为 "blue"(精确字面量)
类型守卫和断言让你能在运行时和类型系统之间安全地穿行。
到现在为止,学的东西基本都在一个文件里。但真实项目代码是分文件的。 下一篇讲模块——怎么组织多文件 TypeScript 项目。