Skip to content

索引类型与索引签名

索引类型让我们能够查询和操作对象类型的键和值类型,是 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') // string
const 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'] // number
type UserName = User['name'] // string
type UserAddress = User['address'] // { city: string; street: string }
// 访问嵌套属性
type City = User['address']['city'] // string
// 使用联合类型访问多个属性
type UserIdOrName = User['id' | 'name'] // number | string
// 结合 keyof
type UserValues = User[keyof User]
// number | string | { city: string; street: string }
// 数组元素类型
type StringArrayElement = string[][number] // string
const colors = ['red', 'green', 'blue'] as const
type 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'> // string
type DbPort = DeepValue<Config, 'database.port'> // number
type 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.keys
function 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,
},
}

总结#

特性语法用途
keyofkeyof T获取类型的所有键
索引访问T[K]获取属性的类型
索引签名[key: string]: T定义动态键类型
模板索引[K in prefix{'{'}string{'}'}]限制键的模式

下一篇我们将学习映射类型,了解如何基于现有类型创建新类型。