Skip to content

类型收窄

类型收窄(Type Narrowing)是 TypeScript 根据代码逻辑自动缩小变量类型范围的能力。理解类型收窄有助于编写更安全的代码。

控制流分析#

TypeScript 通过分析代码的控制流来收窄类型:

// TypeScript 5.x
function example(value: string | number | boolean) {
// value: string | number | boolean
if (typeof value === 'string') {
// value: string
console.log(value.toUpperCase())
return
}
// value: number | boolean(排除了 string)
if (typeof value === 'number') {
// value: number
console.log(value.toFixed(2))
return
}
// value: boolean(排除了 string 和 number)
console.log(value ? '' : '')
}
// 赋值收窄
let x: string | number
x = 'hello'
console.log(x.toUpperCase()) // x: string
x = 42
console.log(x.toFixed(2)) // x: number

真值收窄#

通过真值检查收窄类型:

function printLength(str: string | null | undefined) {
if (str) {
// str: string(排除了 null、undefined、空字符串)
console.log(str.length)
} else {
console.log('空值')
}
}
// 配合逻辑运算符
function greet(name: string | null) {
const displayName = name || '游客'
console.log(`你好,${displayName}`)
}
// 空值合并
function getConfig(config: { timeout?: number }) {
const timeout = config.timeout ?? 3000
console.log(`超时时间: ${timeout}ms`)
}
// 注意:0 和空字符串是假值
function process(value: string | number | null) {
if (value) {
// 这里 value 可能不包含 0 或 ''
console.log(value)
}
}
// 更精确的检查
function processSafe(value: string | number | null) {
if (value !== null) {
// value: string | number
console.log(value)
}
}

相等性收窄#

使用相等性检查收窄类型:

function compare(a: string | number, b: string | boolean) {
if (a === b) {
// 只有 string 是两者的共同类型
// a: string, b: string
console.log(a.toUpperCase())
console.log(b.toLowerCase())
}
}
// 检查特定值
function handle(value: 'success' | 'error' | 'pending') {
if (value === 'success') {
console.log('操作成功')
} else if (value === 'error') {
console.log('操作失败')
} else {
// value: 'pending'
console.log('处理中...')
}
}
// 排除 null
function process(value: string | null) {
if (value !== null) {
// value: string
console.log(value.length)
}
}
// switch 语句
function getColor(status: 'success' | 'warning' | 'error'): string {
switch (status) {
case 'success':
return 'green'
case 'warning':
return 'yellow'
case 'error':
return 'red'
}
}

可辨识联合#

使用公共属性作为类型标识:

// 定义可辨识联合
interface Circle {
kind: 'circle'
radius: number
}
interface Rectangle {
kind: 'rectangle'
width: number
height: number
}
interface Triangle {
kind: 'triangle'
base: number
height: number
}
type Shape = Circle | Rectangle | Triangle
// 使用 kind 属性区分类型
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// shape: Circle
return Math.PI * shape.radius ** 2
case 'rectangle':
// shape: Rectangle
return shape.width * shape.height
case 'triangle':
// shape: Triangle
return (shape.base * shape.height) / 2
}
}
// 实际应用:Redux Action
interface AddTodoAction {
type: 'ADD_TODO'
payload: { text: string }
}
interface RemoveTodoAction {
type: 'REMOVE_TODO'
payload: { id: number }
}
interface ToggleTodoAction {
type: 'TOGGLE_TODO'
payload: { id: number }
}
type TodoAction = AddTodoAction | RemoveTodoAction | ToggleTodoAction
function todoReducer(state: string[], action: TodoAction): string[] {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.payload.text]
case 'REMOVE_TODO':
return state.filter((_, i) => i !== action.payload.id)
case 'TOGGLE_TODO':
return state // 简化示例
}
}

never 类型与穷尽性检查#

使用 never 确保处理了所有情况:

type Shape = Circle | Rectangle | Triangle
function getArea(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
default:
// 如果所有情况都已处理,shape 类型为 never
const _exhaustiveCheck: never = shape
return _exhaustiveCheck
}
}
// 辅助函数
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`)
}
function handleShape(shape: Shape): string {
switch (shape.kind) {
case 'circle':
return `圆形,半径 ${shape.radius}`
case 'rectangle':
return `矩形,${shape.width} x ${shape.height}`
case 'triangle':
return `三角形,底 ${shape.base},高 ${shape.height}`
default:
return assertNever(shape)
}
}
// 新增类型时会报错
interface Square {
kind: 'square'
side: number
}
type ShapeWithSquare = Circle | Rectangle | Triangle | Square
function getAreaNew(shape: ShapeWithSquare): 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 'square':
// return shape.side ** 2;
default:
// ❌ 错误:Square 不能赋值给 never
const _exhaustiveCheck: never = shape
return _exhaustiveCheck
}
}

赋值分析#

TypeScript 跟踪变量的赋值历史:

let value: string | number
// value: string | number(未赋值)
value = 'hello'
console.log(value.toUpperCase()) // value: string
value = 42
console.log(value.toFixed(2)) // value: number
// 条件赋值
function process(flag: boolean) {
let result: string | number
if (flag) {
result = 'success'
} else {
result = 0
}
// result: string | number
console.log(result)
}
// 循环中的赋值
function findFirst(items: (string | number)[]): string | number | undefined {
let found: string | number | undefined
for (const item of items) {
if (typeof item === 'string') {
found = item
break
}
}
return found
}

类型谓词收窄#

结合类型守卫进行收窄:

interface Fish {
swim: () => void
}
interface Bird {
fly: () => void
}
function isFish(pet: Fish | Bird): pet is Fish {
return 'swim' in pet
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim() // pet: Fish
} else {
pet.fly() // pet: Bird
}
}
// 数组过滤
const pets: (Fish | Bird)[] = [
{ swim: () => console.log('游泳') },
{ fly: () => console.log('飞翔') },
{ swim: () => console.log('潜水') },
]
const fishes = pets.filter(isFish)
// fishes: Fish[]
fishes.forEach((fish) => fish.swim())

断言收窄#

使用断言函数收窄类型:

function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new TypeError('Expected string')
}
}
function process(value: unknown) {
assertIsString(value)
// value: string
console.log(value.toUpperCase())
}
// 非空断言
function assertDefined<T>(
value: T | null | undefined,
message?: string
): asserts value is T {
if (value === null || value === undefined) {
throw new Error(message ?? 'Value is null or undefined')
}
}
function getFirstChar(str: string | null): string {
assertDefined(str, 'String cannot be null')
return str.charAt(0)
}

实际应用#

解析配置#

interface BaseConfig {
type: string
}
interface DatabaseConfig extends BaseConfig {
type: 'database'
host: string
port: number
username: string
password: string
}
interface CacheConfig extends BaseConfig {
type: 'cache'
host: string
ttl: number
}
interface FileConfig extends BaseConfig {
type: 'file'
path: string
encoding: string
}
type Config = DatabaseConfig | CacheConfig | FileConfig
function initializeService(config: Config): void {
switch (config.type) {
case 'database':
console.log(`连接数据库: ${config.host}:${config.port}`)
console.log(`用户: ${config.username}`)
break
case 'cache':
console.log(`连接缓存: ${config.host}`)
console.log(`TTL: ${config.ttl}ms`)
break
case 'file':
console.log(`读取文件: ${config.path}`)
console.log(`编码: ${config.encoding}`)
break
}
}

状态机#

type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string }
| { status: 'error'; error: Error }
function renderState(state: State): string {
switch (state.status) {
case 'idle':
return '等待操作...'
case 'loading':
return '加载中...'
case 'success':
return `数据: ${state.data}`
case 'error':
return `错误: ${state.error.message}`
}
}
// 状态转换
function transition(state: State, action: 'start' | 'succeed' | 'fail'): State {
switch (state.status) {
case 'idle':
if (action === 'start') {
return { status: 'loading' }
}
return state
case 'loading':
if (action === 'succeed') {
return { status: 'success', data: '获取的数据' }
}
if (action === 'fail') {
return { status: 'error', error: new Error('请求失败') }
}
return state
default:
return state
}
}

API 响应处理#

type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: { code: number; message: string } }
async function fetchData<T>(url: string): Promise<ApiResult<T>> {
try {
const response = await fetch(url)
const data = await response.json()
return { ok: true, data }
} catch (e) {
return {
ok: false,
error: {
code: 500,
message: e instanceof Error ? e.message : 'Unknown error',
},
}
}
}
async function getUser(id: string) {
const result = await fetchData<{ name: string }>(`/api/users/${id}`)
if (result.ok) {
// result.data: { name: string }
console.log(`用户名: ${result.data.name}`)
} else {
// result.error: { code: number; message: string }
console.log(`错误 ${result.error.code}: ${result.error.message}`)
}
}

常见问题#

🙋 为什么有时类型没有被正确收窄?#

// 回调函数中的收窄问题
function example(value: string | null) {
if (value !== null) {
// value: string
setTimeout(() => {
// value: string | null(回调中类型被重置)
// 因为 TypeScript 不知道回调执行时 value 是否变化
}, 100)
}
}
// 解决方案:保存到局部变量
function exampleFixed(value: string | null) {
if (value !== null) {
const safeValue = value // safeValue: string
setTimeout(() => {
console.log(safeValue.toUpperCase()) // 安全
}, 100)
}
}

🙋 如何收窄对象的属性?#

interface User {
name: string
email: string | null
}
function sendEmail(user: User) {
// 直接检查
if (user.email !== null) {
// user.email: string
console.log(`发送邮件到: ${user.email}`)
}
// 解构后检查
const { email } = user
if (email !== null) {
console.log(`发送邮件到: ${email}`)
}
}

总结#

收窄方式说明示例
typeof检查原始类型typeof x === 'string'
instanceof检查类实例x instanceof Date
in检查属性存在'name' in obj
相等性检查具体值x === 'value'
真值排除假值if (x) { }
可辨识联合公共标识属性switch (x.kind)
类型谓词自定义守卫x is Type
断言函数断言或抛错asserts x is Type

下一篇我们将学习索引类型与索引签名。