在 js 中,访问对象的 key
let user = {
name: "下雪天的夏风",
19: "19value",
};
console.log(user.name);
console.log(user[19]);
user.age = 18
如果key
是对象,会默认执行它的 toString()
方法。
let user = {
name: "下雪天的夏风",
};
const obj = {
toString() {
console.log("be string");
return "_obj";
},
};
user[obj] = "关注一波";
console.log(user);
// be string
// { name: '下雪天的夏风', _obj: '关注一波' }
其他的类型,来看下结果
let user = {};
const obj1 = {
toString() {
console.log("be string");
return "_obj";
},
};
const obj2 = {};
function foo() {}
const sym = Symbol();
user[obj1] = "obj1 的 value";
user[obj2] = "obj2 的 value";
user[foo] = "foo 的 value";
user[sym] = "sym 的 value";
console.log(user); // { name: '下雪天的夏风', _obj: '关注一波' }
/*
be string
{
_obj: 'obj1 的 value',
'[object Object]': 'obj2 的 value',
'function foo() {}': 'foo 的 value',
[Symbol()]: 'sym 的 value'
}
*/
可以看到,对象默认的
toString()
方法执行结果比较特殊。
可以简单的理解:key
就是索引。
基于在 js 中对象和函数等作为索引产生的特殊行为,ts 做了进一步的约束。
索引类型只能是:string
| number
| symbol
,而value
可以是任意类型。
string
也可以是模板字面量interface HandleEvents {
[key: `${string}Changed`]: () => void;
}
.toString()
方法let user: any = {};
const obj1 = {
toString() {
return "_obj1";
},
};
// Type '{ toString(): string; }' cannot be used as an index type.ts(2538)
user[obj1] = "obj1 的 value";
user[obj1.toString()] = "ok";
索引签名就是在约束了索引(key
)类型的基础上,统一定义了对象的 key
和 value
的类型。
换句话说,索引签名可以在只知道key
和value
的类型下,来统一定义对象的类型。
规定:当声明一个索引签名后,所有成员都必须符合索引签名:
举例:
interface Sign1 {
// key 只是占位符,随便什么单词都可以
[key: string]: string;
}
type Sign2 = {
[index: number]: string | number;
};
const foo: {
[aaa: string]: { message: string }; // value 只能是1个对象,并且只有1个属性 message
} = {};
// 必须包含 x 属性
interface Sign1 {
[key: string]: number;
x: number;
}
interface Sign2 {
[key: string]: number;
y: string; // Error: 属性 y 的类型只能是'number'.ts(2411)
}
在多个索引签名存在时,
string
类型的索引最严格,书写时应该包含所有的 value 类型(假设为 All)。
其他类型的索引,对应的 value 类型只能是 All 的子级。
interface Sign3 {
[key: string]: string | number | boolean; // 必须包括所用成员类型
[index: symbol]: string;
[index2: number]: number;
}
即便使用多个索引签名,也有覆盖不到的情况。
比如定义的对象中有一个list:string[]
字段,其他字段都不是string[]
类型。如果按照下面的写法,任何字段都可以是string[]
类型。
interface Sign4 {
[key: string]: string | number | string[]; // 必须包括所用成员类型
}
我们可以用索引签名+联合类型。
type Sign4 = {
[key: string]: string | number;
} | { list: string[] }
先看下 Record
的定义
// node_modules\typescript\lib\lib.es5.d.ts
type Record<K extends keyof any, T> = {
[P in K]: T;
};
鼠标放上去之后,会发现其实 key
和value
的类型的规则和索引签名的一致,也是:string
| number
| symbol
,而value
可以是任意类型。
所以,二者并没有多大的区别,只是使用上略有不同而已。目前只发现的一个区别:
Record 的 key
可以为具体的类型,一般用于创建具有特定键值对的对象类型。
type Type1 = Record<'a | b | c', string>
keyof
type Sign = {
[key: string]: boolean;
}
// string | number
type keys = keyof Sign
const obj: Sign = {};
obj[2] = 123; // ok
obj["3"] = 123;
原因是:当数字作为 key
时,js 会隐式地将数字强制转换为字符串,ts 也会执行这种转换。
这是我的提问
<script setup lang="ts">
interface NumberDictionary {
[index: string]: string;
}
const formInfo: NumberDictionary = {
name: "john",
want: "eat",
};
Object.entries(formInfo).forEach(([key, value]) => {
console.log(key, value);
});
script>
<template>
<form>
<input v-for="(value, key) in formInfo" :name="key" :value="value" :key="key" type="text" />
form>
template>
可以看到2个地方 key
的类型不一致:
原因:
Object.entries
的类型声明// node_modules\typescript\lib\lib.es2017.object.d.ts
entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];
可以看到接受的就是 string
类型,相当于在 forEach
使用时,key
已经被断言为 string
类型了。
v-for
的类型声明// node_modules\@vue\runtime-core\dist\runtime-core.d.ts 第 1602 行
/**
* v-for object
*/
export declare function renderList<T>(
source: T,
renderItem: <K extends keyof T>(
value: T[K],
key: K,
index: number
) => VNodeChild)
: VNodeChild[];
可以看到 K extends keyof T
,很熟悉!这就是上面遇到的第1个问题。
对上面这个例子来说,v-for
中 key
的类型就是 string | number
。
知道原因了,解决方法就有了,用Record
即可
interface NumberDictionary {
[index: string]: string;
}
// ↓↓
type NumberDictionary = Record<string, string>
以上。
参考
索引签名-参考1
索引签名-参考2
索引签名-参考3