keyof / typeof / 索引访问
这三个是"从已有类型里取东西"的工具,也是类型编程里出现频率最高的语法。
keyof:取键
ts
interface Todo {
title: string
done: boolean
priority: number
}
type K = keyof Todo
keyof 返回的是键名的字面量联合。
几个特殊结果:
ts
type A = keyof any
type B = keyof unknown
interface Dict {
[k: string]: number
}
type C = keyof Dict
type D = keyof string[]
keyof Dict 是 string | number 而不是 string,因为 JS 里 obj[0] 等价于 obj['0']。
keyof 与映射类型配合
这是最常见用法:
ts
type Getters<T> = {
[K in keyof T]: () => T[K]
}
interface Todo {
title: string
done: boolean
}
type G = Getters<Todo>
typeof:值 → 类型
typeof 是值层通往类型层的唯一入口。
ts
const config = {
host: 'localhost',
port: 3000,
nested: { debug: true },
}
type Config = typeof config
注意对象属性默认会拓宽('localhost' 变 string),要保住字面量加 as const:
ts
const config = {
host: 'localhost',
port: 3000,
} as const
type Config = typeof config
typeof 的三种位置
ts
const config = { host: 'localhost', port: 3000 } as const
// ① 值的位置:运行时操作符(JS 原本就有)
const a = typeof 1
// ② 类型的位置:取值的类型
const obj = { x: 1 }
type T = typeof obj
// ③ 在 type 查询里嵌套使用
type Host = typeof config['host']
索引访问类型 T[K]
类型层的"查表"。
ts
interface Todo {
title: string
done: boolean
}
type A = Todo['title']
type B = Todo['title' | 'done']
type C = Todo[keyof Todo]
K 是联合时,结果是值的联合——这是它最重要的特性。
数组与元组
ts
type A = string[][number]
type B = [string, number, boolean][number]
type C = [string, number]['length']
type D = string[]['length']
元组能用 [number] 转成联合、能用 ['length'] 拿到具体长度;数组不行(长度是 number)。这个差异是"用元组做计数"的基础。
索引访问会保留可选性
ts
interface Opt {
a?: string
b: number
}
type A = Opt['a']
配合 noUncheckedIndexedAccess 时,索引签名访问也会带 undefined。
三者组合的实战
从常量对象生成联合类型
ts
const ROUTES = {
home: '/',
about: '/about',
user: '/user/:id',
} as const
type RouteKey = keyof typeof ROUTES
type RoutePath = (typeof ROUTES)[RouteKey]
这是"配置即类型"的标准写法:改常量,类型自动跟着变,不用维护两份。
从数组常量生成联合
ts
const STATUSES = ['idle', 'loading', 'done'] as const
type Status = (typeof STATUSES)[number]
类型安全的 get
ts
declare function get<T, K extends keyof T>(obj: T, key: K): T[K]
const todo = { title: 'x', done: false }
const a = get(todo, 'title')
type A = typeof a
const b = get(todo, 'done')
type B = typeof b
返回值类型跟着 key 变,这正是 K extends keyof T + T[K] 组合的威力。
提取嵌套路径
ts
type PathsOf<T> = {
[K in keyof T & string]: T[K] extends Record<string, unknown>
? `${K}.${keyof T[K] & string}`
: K
}[keyof T & string]
type Todo = {
title: string
meta: { author: string; tags: string }
}
type P = PathsOf<Todo>
这个例子把映射类型、模板字面量、索引访问三者组合了起来。做成任意深度需要递归,见递归与元组计数。
容易踩的坑
1. keyof 结果要过滤 symbol / number
ts
type StrKeys<T> = Extract<keyof T, string>
interface M {
a: string
[Symbol.iterator]: () => void
}
type K = StrKeys<M>
2. 模板字面量里要 & string
ts
type Getter<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
}keyof T 可能是 string | number | symbol,而 Capitalize 只接受 string,所以要 & string 收敛。
3. typeof 不能用于类型
ts
type A = { x: 1 }
// type B = typeof A // 报错:A 是类型不是值反过来,值也不能直接当类型用,必须 typeof。