本文献给:
已掌握 TypeScript 接口基本用法、可选属性、只读属性等知识的开发者。本文将带你学习索引签名(动态属性定义)、只读数组的几种写法,以及 keyof 和 typeof 两个类型运算符的基础用法,为后续的映射类型打下基础。
你将学到:
- 字符串索引签名与数字索引签名
- 索引签名与其它属性的组合规则
- 只读数组的三种定义方式
keyof类型运算符提取对象键名typeof类型运算符获取变量/属性的类型
目录
- [一、索引签名(Index Signatures)](#一、索引签名(Index Signatures))
-
- [1.1 字符串索引签名](#1.1 字符串索引签名)
- [1.2 数字索引签名](#1.2 数字索引签名)
- [1.3 两种索引签名的兼容规则](#1.3 两种索引签名的兼容规则)
- [1.4 索引签名与普通属性的组合](#1.4 索引签名与普通属性的组合)
- [1.5 只读索引签名](#1.5 只读索引签名)
- 二、只读数组
-
- [2.1 只读数组的三种定义方式](#2.1 只读数组的三种定义方式)
- [2.2 只读数组的可变性注意](#2.2 只读数组的可变性注意)
- [2.3 只读数组与普通数组的兼容](#2.3 只读数组与普通数组的兼容)
- [三、keyof 类型运算符](#三、keyof 类型运算符)
-
- [3.1 基本用法](#3.1 基本用法)
- [3.2 keyof 与索引签名](#3.2 keyof 与索引签名)
- [3.3 keyof 与类、数组](#3.3 keyof 与类、数组)
- [3.4 使用 keyof 约束泛型](#3.4 使用 keyof 约束泛型)
- [四、typeof 类型运算符](#四、typeof 类型运算符)
-
- [4.1 基本用法](#4.1 基本用法)
- [4.2 typeof 与 keyof 配合](#4.2 typeof 与 keyof 配合)
- [4.3 typeof 与 ReturnType 等工具类型](#4.3 typeof 与 ReturnType 等工具类型)
- [4.4 注意:typeof 与 JavaScript 的运行时 typeof 不同](#4.4 注意:typeof 与 JavaScript 的运行时 typeof 不同)
- 五、常见错误与注意事项
-
- [5.1 索引签名覆盖所有属性导致类型过宽](#5.1 索引签名覆盖所有属性导致类型过宽)
- [5.2 数字索引签名误用于对象](#5.2 数字索引签名误用于对象)
- [5.3 keyof 对于有可选属性的类型](#5.3 keyof 对于有可选属性的类型)
- [5.4 typeof 只能用于变量、属性,不能用于任意表达式](#5.4 typeof 只能用于变量、属性,不能用于任意表达式)
- [5.5 混淆 readonly 修饰符的位置](#5.5 混淆 readonly 修饰符的位置)
- 六、综合示例
- 七、小结
一、索引签名(Index Signatures)
索引签名用于描述对象中动态属性名的类型结构。当不确定对象的属性名具体有哪些,但知道属性值的类型时,可以使用索引签名。
1.1 字符串索引签名
typescript
interface StringDictionary {
[key: string]: string;
}
const dict: StringDictionary = {
hello: "world",
foo: "bar",
// 任意字符串键名,值必须为 string
};
1.2 数字索引签名
JavaScript 中对象的属性名会被转换为字符串,但 TypeScript 区分数字索引和字符串索引,主要用于数组类型描述。
typescript
interface NumberArray {
[index: number]: string;
}
const arr: NumberArray = ["a", "b", "c"];
console.log(arr[0]); // "a"
1.3 两种索引签名的兼容规则
同时存在数字索引和字符串索引时,数字索引值的类型必须是字符串索引值类型的子类型。这是因为数字索引最终会被转换为字符串索引。
typescript
interface Mixed {
[index: number]: number; // 数字索引返回 number
[index: string]: number | string; // ❌ 错误:字符串索引值类型必须包含 number
}
// 正确示例
interface OK {
[index: number]: number;
[index: string]: number; // 可以,两者都是 number
}
更常见的模式:数字索引用于数组元素,字符串索引用于额外属性,此时数字索引值的类型应是字符串索引值类型的子类型。
typescript
interface ArrayLike<T> {
[index: number]: T; // 数字索引返回 T
length: number; // 普通属性
// 如果加上字符串索引,其值类型必须兼容 T
}
1.4 索引签名与普通属性的组合
接口中可以同时包含索引签名和普通属性,但普通属性的类型必须匹配索引签名的值类型。
typescript
interface UserMap {
[id: string]: string; // 所有属性值必须是 string
name: string; // OK
age: number; // ❌ number 不能赋给 string
}
如果希望某些属性类型不同,可以使用联合类型作为索引签名的值类型,或者将特殊属性单独定义。
typescript
interface Flexible {
[key: string]: string | number;
name: string; // OK
age: number; // OK(联合类型包含 number)
}
1.5 只读索引签名
使用 readonly 修饰索引签名,可以防止属性值被修改。
typescript
interface ReadonlyDict {
readonly [key: string]: number;
}
const dict: ReadonlyDict = { a: 1, b: 2 };
dict.a = 3; // ❌ 只读
二、只读数组
2.1 只读数组的三种定义方式
方式一:ReadonlyArray<T> 泛型
typescript
let ro: ReadonlyArray<number> = [1, 2, 3];
ro.push(4); // ❌ 不存在 push
ro[0] = 10; // ❌ 只读
方式二:readonly 修饰符 + 数组类型
typescript
let ro2: readonly number[] = [1, 2, 3];
// 与 ReadonlyArray<number> 等价
方式三:as const 断言
typescript
let ro3 = [1, 2, 3] as const;
// 类型为 readonly [1, 2, 3](只读元组,元素为字面量类型)
ro3[0] = 10; // ❌
2.2 只读数组的可变性注意
只读数组禁止修改数组本身(添加、删除、修改元素),但如果数组元素是引用类型,元素内部属性仍可修改。
typescript
interface User { name: string; }
const users: readonly User[] = [{ name: "Alice" }];
users[0].name = "Bob"; // OK,元素内部可修改
// users[0] = { name: "Charlie" }; // ❌ 不能替换元素
2.3 只读数组与普通数组的兼容
函数参数中,普通数组可以赋给只读数组参数(可读性变宽),但反过来不行。
typescript
function logItems(items: readonly number[]) {
items.forEach(i => console.log(i));
}
const mutable: number[] = [1, 2, 3];
logItems(mutable); // OK,可变数组可传入只读参数
三、keyof 类型运算符
keyof 返回一个类型的所有属性名组成的联合类型。
3.1 基本用法
typescript
interface Person {
name: string;
age: number;
address: string;
}
type PersonKeys = keyof Person; // "name" | "age" | "address"
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const p: Person = { name: "Alice", age: 25, address: "Beijing" };
const n = getProperty(p, "name"); // 类型为 string
const a = getProperty(p, "age"); // 类型为 number
3.2 keyof 与索引签名
如果类型有索引签名,keyof 会包含索引类型(如 string 或 number)。
typescript
interface Dict {
[key: string]: number;
}
type DictKeys = keyof Dict; // string | number(数字也会被转换为字符串)
3.3 keyof 与类、数组
typescript
class MyClass {
x = 0;
y = 0;
}
type ClassKeys = keyof MyClass; // "x" | "y"
type ArrayKeys = keyof Array<number>; // 包括 "length"、"push"、"pop" 等数组方法名
3.4 使用 keyof 约束泛型
typescript
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
const users = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
const names = pluck(users, "name"); // string[]
const ids = pluck(users, "id"); // number[]
四、typeof 类型运算符
typeof 在类型上下文中用于从变量或属性获取其类型。
4.1 基本用法
typescript
const greeting = "hello";
type GreetingType = typeof greeting; // string
const point = { x: 10, y: 20 };
type PointType = typeof point; // { x: number; y: number }
function add(a: number, b: number) { return a + b; }
type AddType = typeof add; // (a: number, b: number) => number
4.2 typeof 与 keyof 配合
先获取对象的类型,再提取其键名。
typescript
const config = {
apiUrl: "https://api.com",
timeout: 5000,
retries: 3
};
type ConfigKeys = keyof typeof config; // "apiUrl" | "timeout" | "retries"
4.3 typeof 与 ReturnType 等工具类型
typeof 常用于配合 ReturnType、Parameters 等内置工具类型,获取函数的返回值类型或参数类型。
typescript
function fetchData(): Promise<{ id: number }> {
return Promise.resolve({ id: 1 });
}
type FetchResult = ReturnType<typeof fetchData>; // Promise<{ id: number }>
4.4 注意:typeof 与 JavaScript 的运行时 typeof 不同
TypeScript 的 typeof 出现在类型注解的位置,会被编译擦除;JavaScript 的 typeof 是运行时运算符。
typescript
const val = "hello";
// 类型上下文的 typeof
type T = typeof val; // string
// 运行时的 typeof
console.log(typeof val); // "string"
五、常见错误与注意事项
5.1 索引签名覆盖所有属性导致类型过宽
typescript
interface TooWide {
[key: string]: string;
createdAt: Date; // ❌ Date 不能赋给 string
}
解决:使用联合类型或明确单独属性。
5.2 数字索引签名误用于对象
数字索引签名主要用于数组或类数组对象。普通对象用字符串索引签名更合适。
5.3 keyof 对于有可选属性的类型
可选属性也会出现在 keyof 的结果中。
typescript
interface User {
name: string;
age?: number;
}
type UserKeys = keyof User; // "name" | "age"
5.4 typeof 只能用于变量、属性,不能用于任意表达式
typescript
type T = typeof (1 + 2); // ❌ 不能在 typeof 中使用表达式
5.5 混淆 readonly 修饰符的位置
数组的 readonly 修饰符应放在类型名前:readonly number[],而不是 number readonly[]。
六、综合示例
typescript
// 1. 索引签名 + keyof 实现类型安全的字典
interface SafeDict<T> {
[key: string]: T;
get(key: string): T | undefined;
set(key: string, value: T): void;
}
class Dict<T> implements SafeDict<T> {
[key: string]: T;
get(key: string): T | undefined {
return this[key];
}
set(key: string, value: T): void {
this[key] = value;
}
}
// 2. 只读数组 + 类型守卫
function safeFirst<T>(arr: readonly T[]): T | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
const colors = ["red", "green", "blue"] as const;
const firstColor = safeFirst(colors); // 类型 "red" | undefined
// 3. keyof + typeof 实现配置校验
const appConfig = {
env: "development",
port: 3000,
debug: true
} as const;
type Config = typeof appConfig;
type ConfigKey = keyof Config; // "env" | "port" | "debug"
function getConfig(key: ConfigKey): Config[ConfigKey] {
return appConfig[key];
}
const env = getConfig("env"); // "development"
const port = getConfig("port"); // 3000
// 4. 结合索引签名和 keyof 约束函数参数
function updateProperty<T extends object, K extends keyof T>(
obj: T,
key: K,
value: T[K]
): void {
obj[key] = value;
}
const user = { name: "Alice", age: 25 };
updateProperty(user, "name", "Bob"); // OK
updateProperty(user, "age", 30); // OK
// updateProperty(user, "name", 123); // ❌ 类型不匹配
七、小结
| 概念 | 语法示例 | 说明 |
|---|---|---|
| 字符串索引签名 | { [key: string]: T } |
动态属性,键名为字符串 |
| 数字索引签名 | { [index: number]: T } |
用于数组或类数组对象 |
| 只读数组 | readonly T[] 或 ReadonlyArray<T> |
不可变数组 |
as const 数组 |
[1,2] as const |
只读字面量元组 |
keyof 运算符 |
keyof T |
提取类型 T 的所有属性名 |
typeof 运算符 |
typeof variable |
获取变量或属性的类型 |
| keyof + typeof 组合 | keyof typeof obj |
获取对象字面量的键名联合类型 |
觉得文章有帮助?别忘了:
👍 点赞 👍 -- 给我一点鼓励
⭐ 收藏 ⭐ -- 方便以后查看
🔔 关注 🔔 -- 获取更新通知
标签: #TypeScript #索引签名 #只读数组 #keyof #typeof #学习笔记 #前端开发