TypeScript { [key: string]: unknown } 索引签名写法和 Record 替代

核心定义

{ [key: string]: unknown } 描述一种对象:索引签名 key :键不固定,但必须是 string 类型;属性值类型固定为 unknown,以接收任何类型的数据。

语法拆解

[key: string] 是索引签名参数。方括号 [] 是必须的语法结构,表示这是一个通配符规则,不是具体属性名。key 只是个占位符,叫 propid 都行。: string 规定了所有属性名必须是字符串。

: unknown 是索引类型的返回值。它表示所有符合 [key: string] 规则的属性,其值都是 unknown 类型。unknown 是类型安全的顶层类型,任何值都能赋给它,但它不能直接赋值给别人,必须先做类型收窄才能用。

为什么要用 unknown 而不是 any?

写成 { [key: string]: any },TypeScript 会彻底关闭类型检查,你乱写 obj.foo.bar 也不报错,容易出现运行时错误。

写成 { [key: string]: unknown },你访问 obj.foo 得到的是 unknown。你必须显式判断类型(如 if (typeof obj.foo === 'string'))或断言,才能操作。这强制你在写代码时就处理风险。

举例:

typescript 复制代码
const data: { [key: string]: unknown } = {
  name: "Alice",
  age: 25
};

// TypeScript: 🛑 "不行!这东西是 unknown,不能直接用"
// data.age.toUpperCase();
// 先检查再用:
if (typeof data.age === 'string') {
    data.age.toUpperCase();
}

等价写法:Record

Record<string, unknown>{ [key: string]: unknown } 在类型系统里完全等价。Record 是语法糖,专门用来描述"键值对"关系,可读性更好。

使用场景

  1. 字典/哈希表结构 。当你需要一个对象当 Map 用,键是动态字符串(ID、URL、配置名),且值的类型未知或统一时。

    typescript 复制代码
    const cache: Record<string, unknown> = {};
    cache['/api/user'] = { id: 1, name: 'A' };
  2. 处理非结构化数据 。比如解析不知道结构的 JSON,或者处理 FormData 这种键值对。

    typescript 复制代码
    const jsonString = '{"id": 101, "meta": {"author": "admin"}, "tags": ["ts", "js"]}';
    
    // 1. 先用 unknown 接住,最安全
    const parsed: unknown = JSON.parse(jsonString);
    
    // 2. 断言成 Record,告诉 TS:作为字典使用
    const data = parsed as Record<string, unknown>;
    
    // 3. 取出来的值还是 unknown,老老实实做判断
    const tags = data['tags'];
    if (Array.isArray(tags)) {
        console.log(tags.join(', ')); // 保证类型安全
    }
  3. 泛型约束 。在泛型里限制一个参数必须是能通过 string 键索引的对象。
    写工具函数时,限制参数必须是个"能按字符串索引的对象",入参可能是从某个 API 返回的,通过该工具函数实现取值类型安全(不确定入参中到底有啥,值的类型也不确定)。

    typescript 复制代码
    // 使用 extends 约束: T 必须是个键值对对象
    function getProperty<T extends Record<string, unknown>>(obj: T, key: string) {
      return obj[key]; // 返回值自动是 unknown
    }
    
    const user = { name: "Bob", age: 30 };
    const val = getProperty(user, "name"); // val 类型是 unknown
    
    // 拿到手还是得自己收窄类型
    if (typeof val === 'string') {
        console.log(val.toUpperCase()); // ✅
    }

关键限制(隐式规则)

这是最容易死的地方:一旦写了索引签名,对象里所有明确写出来的属性,类型必须兼容索引签名的返回值。

例如:

typescript 复制代码
interface UserNames {
  [key: string]: string,
  count: number;
  // Property 'count' of type 'number' is not assignable to 'string' index type 'string'.
}

修正方法 :如果对象有固定属性且类型不一致,必须把索引签名的值类型改成联合类型(如 string | number | boolean),或者用交叉类型(&)。别让固定属性的类型跟索引签名打架。

使用联合类型:

typescript 复制代码
interface UserNames {
    [key: string]: string | number;
    count: number;
}

使用交叉类型:

typescript 复制代码
type Base = Record<string, unknown>;
// 给通用对象 Base 添加约束,必须要有 id 和 name:
type User = Base & { id: number; name: string };

const u: User = {
    id: 1,
    name: "Alice",
    anyOtherKey: "anything"
};
相关推荐
xiaotao1314 小时前
第九章:Vite API 参考手册
前端·vite·前端打包
午安~婉4 小时前
Electron桌面应用聊天(续)
前端·javascript·electron
彧翎Pro5 小时前
基于 RO1 noetic 配置 robosense Helios 32(速腾) & xsense mti 300
前端·jvm
小码哥_常5 小时前
解锁系统设置新姿势:Activity嵌入全解析
前端
之歆5 小时前
前端存储方案对比:Cookie-Session-LocalStorage-IndexedDB
前端
哟哟耶耶5 小时前
vue3-单文件组件css功能(:deep,:slotted,:global,useCssModule,v-bind)
前端·javascript·css
是罐装可乐5 小时前
深入理解“句柄(Handle)“:从浏览器安全到文件系统访问
前端·javascript·安全
华科易迅5 小时前
Vue如何集成封装Axios
前端·javascript·vue.js
康一夏5 小时前
Next.js 13变化有多大?
前端·react·nextjs
糖炒栗子03265 小时前
前端项目标准环境搭建与启动
前端