Skip to content
类型操作符

类型操作符

本章涵盖 TypeScript 中用于操作和变换类型的核心操作符:联合类型(|)、交叉类型(&)、类型级 typeofkeyof、索引访问类型(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)“策略 — 只有通过 typeofinstanceofin 或自定义类型守卫排除其他分支后,才能安全访问特定类型的成员。详细的收窄方法将在下一章展开。

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?因为 stringnumber 是两种完全不重叠的原始类型,任何 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 }

组合模式

typeofkeyofT[K] 串联使用,能够从常量对象中提取出字面量联合类型 — 这是 TypeScript 中最经典的组合模式之一:

const Color = { Red: "#ff0000", Green: "#00ff00", Blue: "#0000ff" } as const

type ColorValue = typeof Color[keyof typeof Color]
// "#ff0000" | "#00ff00" | "#0000ff"

拆解执行过程:

  1. typeof Color{ readonly Red: "#ff0000"; readonly Green: "#00ff00"; readonly Blue: "#0000ff" }
  2. keyof typeof Color"Red" | "Green" | "Blue"
  3. 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 该值一定不是 nullundefined

const el: HTMLDivElement = document.querySelector(".box")!
console.log(el.id)  // ✅ 编译器不再报 null 错误

等价于先用 as 断言:

const el = document.querySelector(".box") as HTMLDivElement

常见使用场景:初始化晚于声明的类属性、DOM 操作、以及你确定某个可选值在当前上下文中一定存在的场合。

🚨 陷阱:非空断言跳过了类型检查。如果你断言的值实际为 nullundefined,运行时仍会报 Cannot read properties of null 之类的错误。只有在逻辑上绝对保证非空时才使用 — 否则优先使用类型守卫或可选链 ?.

🚨 陷阱:注意 nullundefined 在 TypeScript 中的默认行为。当 strictNullChecks 关闭时,nullundefined 可以赋值给任意类型;开启后(推荐),必须显式使用联合类型 string | null | undefined 才能接受二者。