解锁动态键:TypeScript 索引签名完全指南

一、什么是索引签名?

索引签名就是为对象定义一种"规则",规定了"什么样的键"对应"什么样的值"。

语法如下:

typescript 复制代码
interface MyObject {
    [key: KeyType]: ValueType;
}

// 或者使用 type
type MyType = {
    [key: KeyType]: ValueType;
};
  • key: 只是一个占位符,你可以取任何名字,比如 prop, index 等。
  • KeyType: 键的类型。它必须是 string, number, symbol 或由它们组成的联合类型。因为在JavaScript中,对象的键最终都会被转换成字符串。
  • ValueType: 值的类型,可以是任何 TypeScript 类型。
  • 所有成员都必须符合字符串的索引签名

示例

typescript 复制代码
interface ScoreRecord {
    [name: string]: number;
}

const scores: ScoreRecord = {
    'alice': 100,
    'bob': 95,
    'charlie': 98
};

// 访问是类型安全的
const aliceScore: number = scores['alice']; // OK

// 添加新的键值对也必须遵守规则
scores['dave'] = 99; // OK

scores['eve'] = 'A+'; // Error:不能将类型"string"分配给类型"number"

二、注意事项

2.1 可以与其他属性共存,但必须兼容

一个类型可以同时拥有索引签名和明确的属性。但有一个重要前提:所有明确定义的属性,其类型都必须是索引签名值类型的子类型

typescript 复制代码
interface UserProfile {
    // 明确的属性
    id: number;
    name: string;
  
    // 索引签名
    [prop: string]: string | number; // 值的类型是 string 或 number
}

const user: UserProfile = {
    id: 123,       // OK, number 是 string | number 的子类型
    name: "Alice", // OK, string 是 string | number 的子类型
    city: "New York", // OK, 'city' 是 string, "New York" 是 string
    age: 30        // OK, 'age' 是 string, 30 是 number
};


interface InvalidProfile {
    id: number; // Error: 类型"number"的属性"id"不能赋给"string"索引类型"string"
    //索引签名要求所有值都是 string,但 id 是 number,不兼容!
    [prop: string]: string; // 
}

2.2 number 类型的键是 string 类型键的"特例"

因为 JavaScript 会将数字键转换为字符串键(例如 obj[5] 等同于 obj['5']),TypeScript 也遵循这个规则。所以,如果你同时定义了 stringnumber 的索引签名,number 索引签名的值类型必须是 string 索引签名值类型的子类型。

typescript 复制代码
interface DataCache {
    [key: string]: any;
    [index: number]: string; // OK, string 是 any 的子类型
}

interface InvalidDataCache {
    [key: string]: string;
    [index: number]: number; // Error! "number"索引类型"number"不能分配给"string"索引类型"string"
}

2.3 访问不存在的属性

typescript 复制代码
interface ScoreRecord {
    [name: string]: number;
}

const scores: ScoreRecord = { 'alice': 100 };
const bobScore = scores['bob']; // `bobScore` 的类型是 number,而不是 number | undefined
console.log(bobScore); // 输出:undefined
console.log(bobScore.toFixed(2)); // 运行时错误!TypeError: Cannot read properties of undefined (reading 'toFixed')

strictNullChecks 模式下,这会成为一个安全隐患。如何解决?

  1. 明确声明 undefined:这是最推荐的方式。

    typescript 复制代码
    interface ScoreRecord {
        [name: string]: number | undefined;
    }
    const scores: ScoreRecord = { 'alice': 100 };
    const bobScore = scores['bob']; // `bobScore` 的类型现在是 number | undefined
    if (bobScore) {
        console.log(bobScore.toFixed(2)); // OK,类型被收窄
    }
  2. 使用 noUncheckedIndexedAccess :在 tsconfig.json 中开启此选项,TypeScript 会自动在索引签名的结果中加入 | undefined

typescript 复制代码
interface ScoreRecord {
    [name: string]: number;
}
const scores: ScoreRecord = { 'alice': 100 };
const bobScore = scores['bob']; // `bobScore` 的类型现在是 number | undefined
console.log(bobScore?.toFixed(2));// 在vscode中调用`toFixed()`时自动会补全`?`判断undefined

三、现代替代方案 Record<K, T>

Record<string, number> 和我们之前的 ScoreRecord 接口几乎是等价的:

typescript 复制代码
// 使用 Record 工具类型
type ScoreRecord = Record<string, number>;

const scores: ScoreRecord = {
    'alice': 100,
    'bob': 95,
};

为什么推荐 Record

  • 可读性更好Record<string, number> 清晰地表达了"一个键为字符串、值为数字的记录"。
  • 更灵活Keys 参数不限于 stringnumber,它可以是具体的字面量联合类型,从而创建更精确的对象类型。
typescript 复制代码
type UserRole = 'admin' | 'user' | 'guest';

// 创建一个确保每种角色都存在的配置对象
const roleConfig: Record<UserRole, { permissions: string[] }> = {
    admin: { permissions: ['create', 'read', 'update', 'delete'] },
    user: { permissions: ['read', 'update'] },
    //guest: { permissions: ['read'] },  //如果你漏掉了一个角色,TypeScript 会报错!
};

这是索引签名无法做到的。当你的键集合是已知且有限 的时,Record 或**映射类型(Mapped Types)**是比索引签名更好的选择。

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货

相关推荐
先吃饱再说6 分钟前
React 组件通信:从 Props 单向传递到自定义事件
前端·react.js·前端框架
swipe10 分钟前
05|(前端转后全栈)不手写一堆 SQL,后端怎么操作数据库?MyBatis-Plus 入门
前端·后端·全栈
Allshadow11 分钟前
Nest.js - 连接数据库
javascript·nestjs
冰暮流星15 分钟前
javascript之date对象
开发语言·javascript·ecmascript
swipe15 分钟前
04|(前端转后全栈)前端状态为什么不够用?从页面数据到 MySQL 持久化
前端·后端·全栈
橘子星16 分钟前
别光看教程!手把手拆解 React Todo 项目,一文吃透 5 个核心概念
前端·javascript
大白要努力!22 分钟前
纯前端实现 PDF 加水印工具 —— 零后端、支持中文、实时预览
前端·pdf·html
石小石Orz24 分钟前
TRAE SOLO实战:实现一个桌面3D助手
前端·人工智能
蜡台29 分钟前
使用 uni-popup 实现数据选择器Data-Picker
前端·javascript·html·uniapp·uni-popup·data-picker
拆房老料31 分钟前
BaseMetas FileView 1.2.0 发布:Office/WPS 大文件预览与内存安全优化实测
前端·产品运营·开源软件