类型收窄(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 = 42console.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('处理中...') }}
// 排除 nullfunction 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 Actioninterface 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 = 42console.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 |
下一篇我们将学习索引类型与索引签名。