TypeScript 索引签名、只读数组与 keyof / typeof 入门

本文献给:

已掌握 TypeScript 接口基本用法、可选属性、只读属性等知识的开发者。本文将带你学习索引签名(动态属性定义)、只读数组的几种写法,以及 keyoftypeof 两个类型运算符的基础用法,为后续的映射类型打下基础。

你将学到:

  1. 字符串索引签名与数字索引签名
  2. 索引签名与其它属性的组合规则
  3. 只读数组的三种定义方式
  4. keyof 类型运算符提取对象键名
  5. 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 会包含索引类型(如 stringnumber)。

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 #学习笔记 #前端开发

相关推荐
笨笨饿2 小时前
#79_NOP()嵌入式C语言中内联汇编宏的抽象封装模式研究
linux·c语言·网络·驱动开发·算法·硬件工程·个人开发
fish_xk2 小时前
Linux的权限
linux·运维·服务器
嵌入式×边缘AI:打怪升级日志4 小时前
Linux 驱动与应用开发核心自测题库(面试官问答完整版)
linux·运维·php
薛定谔的悦5 小时前
储能充放电状态机执行逻辑详解
linux·数据库·能源·储能·bms
嵌入式×边缘AI:打怪升级日志6 小时前
Tina SDK Linux Kernel 基本使用(实战篇:为7寸RGB LCD触摸屏添加驱动支持).md
linux·运维·服务器
前端之虎陈随易6 小时前
为什么今天还会有新语言?MoonBit 想解决什么问题?
大数据·linux·javascript·人工智能·算法·microsoft·typescript
G.晴天6 小时前
Linux常用命令练习流程
java·linux·运维·服务器·tomcat
嵌入式×边缘AI:打怪升级日志6 小时前
Linux 驱动开发核心自测题库(面试官问答版)
linux·运维·驱动开发
想唱rap6 小时前
传输层协议之UDP
java·linux·网络·c++·网络协议·mysql·udp