Skip to content

类型守卫

类型守卫(Type Guard)是 TypeScript 中用于在运行时检查类型的表达式,可以帮助编译器在特定代码块中确定变量的具体类型。

typeof 类型守卫#

使用 typeof 检查原始类型:

// TypeScript 5.x
function process(value: string | number) {
if (typeof value === 'string') {
// 这里 value 被收窄为 string
console.log(value.toUpperCase())
console.log(value.length)
} else {
// 这里 value 被收窄为 number
console.log(value.toFixed(2))
console.log(value * 2)
}
}
process('hello') // HELLO, 5
process(3.14159) // 3.14, 6.28318
// typeof 支持的类型
function checkType(value: unknown) {
if (typeof value === 'string') {
return 'string'
}
if (typeof value === 'number') {
return 'number'
}
if (typeof value === 'boolean') {
return 'boolean'
}
if (typeof value === 'function') {
return 'function'
}
if (typeof value === 'object') {
return 'object or null'
}
if (typeof value === 'undefined') {
return 'undefined'
}
if (typeof value === 'symbol') {
return 'symbol'
}
if (typeof value === 'bigint') {
return 'bigint'
}
}

instanceof 类型守卫#

使用 instanceof 检查类实例:

class Dog {
bark() {
console.log('汪汪!')
}
}
class Cat {
meow() {
console.log('喵喵!')
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark() // animal 是 Dog
} else {
animal.meow() // animal 是 Cat
}
}
const dog = new Dog()
const cat = new Cat()
makeSound(dog) // 汪汪!
makeSound(cat) // 喵喵!
// 处理内置类型
function processValue(value: Date | string[]) {
if (value instanceof Date) {
console.log(value.toISOString())
} else {
console.log(value.join(', '))
}
}
processValue(new Date()) // 2024-02-05T...
processValue(['a', 'b', 'c']) // a, b, c
// 处理错误
function handleError(error: unknown) {
if (error instanceof Error) {
console.log(error.message)
console.log(error.stack)
} else if (typeof error === 'string') {
console.log(error)
} else {
console.log('未知错误')
}
}

in 操作符类型守卫#

使用 in 检查属性是否存在:

interface Bird {
fly(): void
layEggs(): void
}
interface Fish {
swim(): void
layEggs(): void
}
function move(animal: Bird | Fish) {
if ('fly' in animal) {
animal.fly() // animal 是 Bird
} else {
animal.swim() // animal 是 Fish
}
}
// 常用于可辨识联合
interface Circle {
kind: 'circle'
radius: number
}
interface Rectangle {
kind: 'rectangle'
width: number
height: number
}
type Shape = Circle | Rectangle
function getArea(shape: Shape): number {
if ('radius' in shape) {
return Math.PI * shape.radius ** 2
} else {
return shape.width * shape.height
}
}
// 检查可选属性
interface User {
name: string
email?: string
phone?: string
}
function contact(user: User) {
if ('email' in user && user.email) {
console.log(`发送邮件到: ${user.email}`)
} else if ('phone' in user && user.phone) {
console.log(`拨打电话: ${user.phone}`)
} else {
console.log('没有联系方式')
}
}

自定义类型守卫#

使用 is 关键字创建自定义类型守卫:

// 基本语法
function isString(value: unknown): value is string {
return typeof value === 'string'
}
function isNumber(value: unknown): value is number {
return typeof value === 'number'
}
function process(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase()) // value 是 string
} else if (isNumber(value)) {
console.log(value.toFixed(2)) // value 是 number
}
}
// 检查对象类型
interface User {
id: number
name: string
email: string
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value &&
typeof (value as User).id === 'number' &&
typeof (value as User).name === 'string' &&
typeof (value as User).email === 'string'
)
}
function greet(data: unknown) {
if (isUser(data)) {
console.log(`Hello, ${data.name}!`) // 类型安全
} else {
console.log('Invalid user data')
}
}
// 检查数组
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string')
}
function processArray(data: unknown) {
if (isStringArray(data)) {
data.forEach((s) => console.log(s.toUpperCase()))
}
}

断言函数#

使用 asserts 关键字创建断言函数:

// 断言函数:如果检查失败则抛出错误
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value is not a string')
}
}
function process(value: unknown) {
assertIsString(value)
// 之后 value 被视为 string
console.log(value.toUpperCase())
}
// 断言非空
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Value is null or undefined')
}
}
function getLength(str: string | null) {
assertDefined(str)
return str.length // str 是 string
}
// 断言条件
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message)
}
}
function divide(a: number, b: number) {
assert(b !== 0, 'Divisor cannot be zero')
return a / b
}

类型守卫与泛型#

// 泛型类型守卫
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined
}
const values: (string | null | undefined)[] = ['a', null, 'b', undefined, 'c']
const defined = values.filter(isDefined)
// defined: string[]
// 带约束的泛型守卫
interface WithId {
id: string
}
function hasId<T>(value: T): value is T & WithId {
return typeof value === 'object' && value !== null && 'id' in value
}
function processItem<T>(item: T) {
if (hasId(item)) {
console.log(`ID: ${item.id}`)
}
}
// 类型谓词与映射
function isNotNull<T>(value: T | null): value is T {
return value !== null
}
const items: (number | null)[] = [1, null, 2, null, 3]
const numbers = items.filter(isNotNull)
// numbers: number[]

实际应用#

API 响应验证#

interface SuccessResponse<T> {
success: true
data: T
}
interface ErrorResponse {
success: false
error: string
}
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse
function isSuccess<T>(
response: ApiResponse<T>
): response is SuccessResponse<T> {
return response.success === true
}
interface User {
id: number
name: string
}
async function fetchUser(id: number): Promise<User | null> {
const response: ApiResponse<User> = await fetch(`/api/users/${id}`).then(
(r) => r.json()
)
if (isSuccess(response)) {
return response.data // 类型安全
} else {
console.error(response.error)
return null
}
}

表单数据验证#

interface FormData {
username: string
email: string
age: number
}
interface ValidationResult {
valid: boolean
errors: string[]
}
function isValidFormData(data: unknown): data is FormData {
if (typeof data !== 'object' || data === null) {
return false
}
const obj = data as Record<string, unknown>
return (
typeof obj.username === 'string' &&
obj.username.length >= 3 &&
typeof obj.email === 'string' &&
obj.email.includes('@') &&
typeof obj.age === 'number' &&
obj.age >= 0 &&
obj.age <= 150
)
}
function processForm(data: unknown): ValidationResult {
if (isValidFormData(data)) {
// 安全处理表单数据
console.log(`用户: ${data.username}, 邮箱: ${data.email}`)
return { valid: true, errors: [] }
}
return { valid: false, errors: ['Invalid form data'] }
}

事件处理#

interface MouseEvent {
type: 'click' | 'mousemove'
x: number
y: number
}
interface KeyboardEvent {
type: 'keydown' | 'keyup'
key: string
code: string
}
type AppEvent = MouseEvent | KeyboardEvent
function isMouseEvent(event: AppEvent): event is MouseEvent {
return event.type === 'click' || event.type === 'mousemove'
}
function isKeyboardEvent(event: AppEvent): event is KeyboardEvent {
return event.type === 'keydown' || event.type === 'keyup'
}
function handleEvent(event: AppEvent) {
if (isMouseEvent(event)) {
console.log(`鼠标位置: (${event.x}, ${event.y})`)
} else if (isKeyboardEvent(event)) {
console.log(`按键: ${event.key}`)
}
}

常见问题#

🙋 typeof null 返回什么?#

// typeof null 返回 'object',这是 JavaScript 的历史遗留问题
function isObject(value: unknown): value is object {
return typeof value === 'object' && value !== null
}
console.log(typeof null) // 'object'
console.log(isObject(null)) // false
console.log(isObject({})) // true

🙋 instanceof 对原始类型有效吗?#

// instanceof 只对对象有效
const str = 'hello'
console.log(str instanceof String) // false
const strObj = new String('hello')
console.log(strObj instanceof String) // true
// 使用 typeof 检查原始类型
console.log(typeof str === 'string') // true

🙋 如何组合多个类型守卫?#

function isStringOrNumber(value: unknown): value is string | number {
return typeof value === 'string' || typeof value === 'number'
}
function isArrayOfStringsOrNumbers(
value: unknown
): value is (string | number)[] {
return Array.isArray(value) && value.every(isStringOrNumber)
}

总结#

类型守卫语法适用场景
typeoftypeof x === 'type'原始类型检查
instanceofx instanceof Class类实例检查
in'prop' in obj属性存在检查
自定义守卫value is Type复杂类型检查
断言函数asserts value is Type验证并抛出错误

下一篇我们将深入学习类型收窄,了解 TypeScript 如何通过控制流分析收窄类型。