类型操作符
本章涵盖 TypeScript 中用于操作和变换类型的核心操作符:联合类型(|)、交叉类型(&)、类型级 typeof、keyof、索引访问类型(T[K])、类型断言(as)、as const 以及非空断言(!),帮助你灵活组合与推导类型。
union 联合类型
联合类型用 | 连接多个类型,表示值可以是其中任意一种:
let id: string | number
id = "abc" // ✅
id = 42 // ✅
id = true // ❌ 类型 "boolean" 不能赋值给 "string | number"
访问联合类型的变量时,只能访问所有成员共有的属性/方法:
function printId(id: string | number) {
console.log(id.toString()) // ✅ toString 在 string 和 number 上都存在
// console.log(id.toUpperCase()) // ❌ toUpperCase 仅在 string 上存在
}🔬 深入原理:TS 对联合类型采用"类型收窄(Narrowing)“策略 — 只有通过
typeof、instanceof、in或自定义类型守卫排除其他分支后,才能安全访问特定类型的成员。详细的收窄方法将在下一章展开。
function printId(id: string | number) {
if (typeof id === "string") {
console.log(id.toUpperCase()) // ✅ 此分支中 id 已收窄为 string
} else {
console.log(id) // ✅ 此分支中 id 已收窄为 number
}
}never 在联合类型中会被自动消除:
type Foo = never | string | number // string | number
intersection 交叉类型
交叉类型用 & 连接多个类型,将它们的属性合并在一起,新类型必须同时满足所有成员约束:
interface A { name: string }
type B = { age: number }
type C = A & B
let c: C = { name: "Willow", age: 18 } // ✅
当两个类型有同名属性但类型不同时,该属性的类型变为 never:
let a = { name: "Willow" }
let b = { age: 18, name: true }
type Foo = typeof a & typeof b
// let c: Foo = { age: 18, name: "Willow" } // ❌ 不能将 "string" 分配给 "never"
可以使用 Pick 工具类型剔除冲突属性来解决:
type Foo = typeof a & Pick<typeof b, "age">
let c: Foo = { age: 30, name: "Willow" } // ✅
string & number 的结果是 never,因为没有值能同时满足两者:
type Foo = string & number // never
type Foo2 = "a" & "b" // never
联合类型之间的交叉运算会做分发(分配律):
type HD = ("a" | "b") & "a" // "a"
type HD2 = ("a" | "b") & ("a" | string) // "a" | "b"
🔬 深入原理:
string & number的结果为什么是never?因为string和number是两种完全不重叠的原始类型,任何 JavaScript 值在运行时都不可能同时是 string 和 number。TypeScript 的类型系统能够识别这种不可能的情况并给出never。
交叉类型常用于组合已有类型、定义泛型约束:
type User = { name: string; age: number }
type Member = { avatar: string } & User
let member: Member = {
name: "Willow", avatar: "avatar.png", age: 18
}typeof(类型上下文)
TypeScript 中 typeof 有两种身份:
- 运行时
typeof:返回 JavaScript 类型字符串("string","number"等) - 类型级
typeof:从已有值推导出 TypeScript 类型
const config = { host: "localhost", port: 8080 }
// 运行时
console.log(typeof config) // "object"
// 类型级 — 从值推导出类型
type Config = typeof config
// { host: string; port: number }
typeof 的核心价值在于避免冗余的类型定义 — 类型直接从运行时值推导,保持单一数据源:
const COLORS = { red: "#ff0000", green: "#00ff00", blue: "#0000ff" }
type Colors = typeof COLORS
// { red: string; green: string; blue: string }
// 函数返回值的类型也可以这样拿到
function createUser(name: string, age: number) {
return { name, age, createdAt: new Date() }
}
type User = ReturnType<typeof createUser>
// { name: string; age: number; createdAt: Date }
keyof
keyof 获取对象类型所有键名的联合类型:
type User = { name: string; age: number }
type UserKeys = keyof User // "name" | "age"
最经典的应用是配合泛型约束,实现类型安全的属性访问:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: "Willow", age: 18 }
getProperty(user, "name") // ✅ 返回 string,有代码提示
getProperty(user, "email") // ❌ "email" 不在 "name" | "age" 中
对于索引签名类型,keyof 返回索引能接受的类型:
type Dict = { [k: string]: unknown }
type DictKeys = keyof Dict // string | number
// 注意:在 JS 中 obj[0] 等价于 obj["0"],因此 number 也在 keyof 中
索引访问类型 T[K]
用类似取属性值的语法来获取某个属性的类型:
type User = { name: string; age: number; address: { city: string } }
type Age = User["age"] // number
type City = User["address"]["city"] // string(链式访问)
传入联合类型一次性获取多个属性的类型:
type NameOrAge = User["name" | "age"] // string | number
type AllValues = User[keyof User] // string | number | { city: string }
组合模式
将 typeof、keyof 和 T[K] 串联使用,能够从常量对象中提取出字面量联合类型 — 这是 TypeScript 中最经典的组合模式之一:
const Color = { Red: "#ff0000", Green: "#00ff00", Blue: "#0000ff" } as const
type ColorValue = typeof Color[keyof typeof Color]
// "#ff0000" | "#00ff00" | "#0000ff"
拆解执行过程:
typeof Color→{ readonly Red: "#ff0000"; readonly Green: "#00ff00"; readonly Blue: "#0000ff" }keyof typeof Color→"Red" | "Green" | "Blue"typeof Color["Red" | "Green" | "Blue"]→"#ff0000" | "#00ff00" | "#0000ff"
这个模式常用于替代 enum,获得零运行时开销的常量映射。
类型断言(as)
类型断言告诉编译器"我知道这个值是什么类型”,跳过类型检查。有两种等价语法:
const el = document.querySelector(".box") as HTMLDivElement
// 或使用尖括号语法
const el2 = <HTMLDivElement>document.querySelector(".box")🚨 陷阱:断言不是类型转换!它仅在编译时生效,运行时不会改变值的实际类型。如果断言错误,运行时仍然会拿到错误类型的值。
const foo = (a: number) => a ? "hello" : 18
let result = foo(1) as boolean
console.log(typeof result) // "string"
console.log(result) // "hello"
console.log(result == true) // false — 断言没有真的把它变成 boolean
使用场景:
- 收窄
unknown类型 - 覆盖编译器推断不准确的类型(如 DOM API)
- 在逐步迁移 JS 项目时标注临时类型
💡 最佳实践:
<>语法在 JSX/TSX 中与标签冲突,项目中使用as语法更通用。
as const
as const 告诉编译器推断出最窄、最特定的类型,同时将所有值标记为 readonly:
// 普通推断 - 宽泛类型
let name = "Willow" // let name: string
let nums = [1, 2, 3] // let nums: number[]
let user = { name: "Willow", age: 18 }
// { name: string; age: number }
// as const - 字面量 + 只读
let nameConst = "Willow" as const // let nameConst: "Willow"
let numsConst = [1, 2, 3] as const // readonly [1, 2, 3]
let userConst = { name: "Willow", age: 18 } as const
// { readonly name: "Willow"; readonly age: 18 }
数组中混合变量和字面量时,变量保留宽泛类型,字面量收窄:
let a = "Willow"
let b = 18
let arr = [a, b, "forest", true] as const
// readonly [string, number, "forest", true]
as const 在函数返回值中的实用模式 — 解构时自动保留具体类型:
function foo() {
let a = "Willow"
let b = (x: number, y: number): number => x + y
return [a, b] as const
}
let [x, y] = foo() // x: "Willow", y: (x: number, y: number) => number
console.log(y(1, 2)) // ✅
// 不使用 as const 时,x 和 y 会被推断为 string | ((x: number, y: number) => number)
💡 最佳实践:使用
as const替代 enum 定义常量映射,零运行时开销,且更符合 JavaScript 原生习惯。
非空断言 !
在值后面加 ! 告诉 TypeScript 该值一定不是 null 或 undefined:
const el: HTMLDivElement = document.querySelector(".box")!
console.log(el.id) // ✅ 编译器不再报 null 错误
等价于先用 as 断言:
const el = document.querySelector(".box") as HTMLDivElement常见使用场景:初始化晚于声明的类属性、DOM 操作、以及你确定某个可选值在当前上下文中一定存在的场合。
🚨 陷阱:非空断言跳过了类型检查。如果你断言的值实际为
null或undefined,运行时仍会报Cannot read properties of null之类的错误。只有在逻辑上绝对保证非空时才使用 — 否则优先使用类型守卫或可选链?.。
🚨 陷阱:注意
null与undefined在 TypeScript 中的默认行为。当strictNullChecks关闭时,null和undefined可以赋值给任意类型;开启后(推荐),必须显式使用联合类型string | null | undefined才能接受二者。