索引类型让我们能够查询和操作对象类型的键和值类型,是 TypeScript 类型系统中非常强大的特性。
keyof 操作符#
keyof 获取类型的所有键组成的联合类型:
// TypeScript 5.x
interface User { id: number name: string email: string age: number}
// 获取所有键type UserKeys = keyof User// 'id' | 'name' | 'email' | 'age'
// 使用 keyof 约束参数function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]}
const user: User = { id: 1, name: '张三', email: 'test@test.com', age: 25,}
const name = getProperty(user, 'name') // stringconst age = getProperty(user, 'age') // number// getProperty(user, 'invalid'); // ❌ 错误
// keyof 与数组type ArrayKeys = keyof string[]// number | 'length' | 'push' | 'pop' | ...
type TupleKeys = keyof [string, number]// '0' | '1' | 'length' | ...索引访问类型#
使用 T[K] 访问类型的属性类型:
interface User { id: number name: string email: string address: { city: string street: string }}
// 访问单个属性类型type UserId = User['id'] // numbertype UserName = User['name'] // stringtype UserAddress = User['address'] // { city: string; street: string }
// 访问嵌套属性type City = User['address']['city'] // string
// 使用联合类型访问多个属性type UserIdOrName = User['id' | 'name'] // number | string
// 结合 keyoftype UserValues = User[keyof User]// number | string | { city: string; street: string }
// 数组元素类型type StringArrayElement = string[][number] // string
const colors = ['red', 'green', 'blue'] as consttype Color = (typeof colors)[number] // 'red' | 'green' | 'blue'索引签名#
定义动态键的对象类型:
// 字符串索引签名interface StringDictionary { [key: string]: string}
const dict: StringDictionary = { name: '张三', city: '北京', // age: 25 // ❌ 错误:number 不能赋值给 string}
// 数字索引签名interface NumberArray { [index: number]: string}
const arr: NumberArray = ['a', 'b', 'c']console.log(arr[0]) // 'a'
// 混合签名interface MixedDictionary { [key: string]: string | number length: number // 必须兼容索引签名类型 name: string}
// 只读索引签名interface ReadonlyDictionary { readonly [key: string]: string}
const readonlyDict: ReadonlyDictionary = { name: '张三',}// readonlyDict.name = '李四'; // ❌ 错误:只读模板字面量索引#
// 模板字面量作为索引type EventHandlers = { [K in `on${string}`]: (event: Event) => void}
const handlers: EventHandlers = { onClick: (e) => console.log('clicked'), onHover: (e) => console.log('hovered'), // invalid: () => {} // ❌ 不匹配模式}
// 更精确的模式type MouseEvents = 'click' | 'mousedown' | 'mouseup'
type MouseHandlers = { [K in `on${Capitalize<MouseEvents>}`]: (event: MouseEvent) => void}
const mouseHandlers: MouseHandlers = { onClick: (e) => console.log(e.clientX), onMousedown: (e) => console.log(e.button), onMouseup: (e) => console.log('released'),}索引类型实战#
类型安全的属性访问#
// 安全的 pick 函数function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { const result = {} as Pick<T, K> keys.forEach((key) => { result[key] = obj[key] }) return result}
interface User { id: number name: string email: string password: string}
const user: User = { id: 1, name: '张三', email: 'test@test.com', password: 'secret',}
const publicUser = pick(user, ['id', 'name', 'email'])// { id: number; name: string; email: string }
// 安全的 omit 函数function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> { const result = { ...obj } keys.forEach((key) => { delete result[key] }) return result as Omit<T, K>}
const safeUser = omit(user, ['password'])// { id: number; name: string; email: string }动态属性更新#
function updateProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T { return { ...obj, [key]: value }}
interface Settings { theme: 'light' | 'dark' fontSize: number notifications: boolean}
const settings: Settings = { theme: 'light', fontSize: 14, notifications: true,}
const updated = updateProperty(settings, 'theme', 'dark')// updateProperty(settings, 'theme', 'blue'); // ❌ 错误
// 批量更新function updateProperties<T>(obj: T, updates: Partial<T>): T { return { ...obj, ...updates }}
const newSettings = updateProperties(settings, { theme: 'dark', fontSize: 16,})路径访问类型#
// 获取嵌套对象的类型type DeepValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? DeepValue<T[K], Rest> : never : P extends keyof T ? T[P] : never
interface Config { database: { host: string port: number credentials: { username: string password: string } } server: { port: number }}
type DbHost = DeepValue<Config, 'database.host'> // stringtype DbPort = DeepValue<Config, 'database.port'> // numbertype Username = DeepValue<Config, 'database.credentials.username'> // string
// 获取嵌套对象值的函数function getDeepValue<T, P extends string>(obj: T, path: P): DeepValue<T, P> { const keys = path.split('.') let result: any = obj for (const key of keys) { result = result[key] } return result}记录类型#
// 创建特定键值对类型type UserRole = 'admin' | 'user' | 'guest'
type RolePermissions = Record<UserRole, string[]>
const permissions: RolePermissions = { admin: ['read', 'write', 'delete', 'manage'], user: ['read', 'write'], guest: ['read'],}
// 结合 keyof 使用function hasPermission(role: UserRole, permission: string): boolean { return permissions[role].includes(permission)}
// 动态键类型type ApiEndpoints = { [K in 'users' | 'posts' | 'comments']: `/${K}` | `/${K}/:id`}
const endpoints: ApiEndpoints = { users: '/users', posts: '/posts/:id', comments: '/comments',}表单字段类型#
interface FormSchema { username: { type: 'text'; required: true } email: { type: 'email'; required: true } age: { type: 'number'; required: false } bio: { type: 'textarea'; required: false }}
// 从 schema 推断表单值类型type FieldTypeMap = { text: string email: string number: number textarea: string}
type FormValues< T extends Record<string, { type: keyof FieldTypeMap; required: boolean }>,> = { [K in keyof T]: T[K]['required'] extends true ? FieldTypeMap[T[K]['type']] : FieldTypeMap[T[K]['type']] | undefined}
type MyFormValues = FormValues<FormSchema>// {// username: string// email: string// age: number | undefined// bio: string | undefined// }索引签名与其他属性#
// 索引签名与具体属性共存interface Config { name: string // 必须兼容索引签名 version: number // ❌ 如果索引签名是 string,这会报错 [key: string]: string | number // 需要包含具体属性的类型}
// 更好的做法:使用交叉类型interface SpecificConfig { name: string version: number}
type FlexibleConfig = SpecificConfig & { [key: string]: unknown}
const config: FlexibleConfig = { name: 'my-app', version: 1, customField: 'value', anotherField: 123,}
// 使用 Record 辅助type ConfigWithExtras = { name: string version: number} & Record<string, unknown>常见问题#
🙋 keyof 和 Object.keys 有什么区别?#
interface User { id: number name: string}
// keyof 在编译时获取类型type Keys = keyof User // 'id' | 'name'
// Object.keys 在运行时获取值const user: User = { id: 1, name: '张三' }const keys = Object.keys(user) // string[]
// 类型安全的 Object.keysfunction typedKeys<T extends object>(obj: T): (keyof T)[] { return Object.keys(obj) as (keyof T)[]}
const userKeys = typedKeys(user) // ('id' | 'name')[]🙋 如何限制索引签名的键?#
// 使用模板字面量限制type DataPrefix = { [K in `data-${string}`]: string}
const attrs: DataPrefix = { 'data-id': '123', 'data-name': 'test', // 'invalid': 'value' // ❌ 错误}
// 使用 symbol 作为私有键const privateKey = Symbol('private')
interface WithPrivate { [privateKey]: string public: string}🙋 索引签名会影响类型检查吗?#
interface Loose { [key: string]: any name: string}
const obj: Loose = { name: '张三', anything: 123, // any 类型,失去类型检查}
// 更安全的做法interface Strict { name: string metadata?: Record<string, unknown>}
const safeObj: Strict = { name: '张三', metadata: { anything: 123, },}总结#
| 特性 | 语法 | 用途 |
|---|---|---|
| keyof | keyof T | 获取类型的所有键 |
| 索引访问 | T[K] | 获取属性的类型 |
| 索引签名 | [key: string]: T | 定义动态键类型 |
| 模板索引 | [K in | 限制键的模式 |
下一篇我们将学习映射类型,了解如何基于现有类型创建新类型。