网站搜索

typescript - 如何声明具有任意字符串键的类型化对象 - 缺少索引签名?


我需要声明一个对象,该对象具有任意字符串键和值类型为 number 或 string,即像这个问题中那样的东西。

所以我写了

interface AnyObjectWithNumberAndStringValues {
  [s: string]: number | string;
}

和一个测试函数

function f(x: AnyObjectWithNumberAndStringValues) {
  console.log(x);
}

它有点管用。我可以打电话

f({ id: 123, name: "noname" })

但现在,我有一个接口和一个对象,如

interface ISupplier {
  id: number;
  name: string;
}

const o: ISupplier = { id: 123, name: "noname" };

并调用 f(o) 会产生以下错误:

类型 'ISupplier' 的参数不能分配给类型 'AnyObjectWithNumberAndStringValues' 的参数。类型 'ISupplier' 中缺少索引签名.ts(2345)

  • 有人能解释一下为什么吗?
  • 我怎样才能绕过这个问题?

更具体地说:我如何声明 f 接受任何对象,只要其值仅为数字和字符串?

请参阅此沙盒获取完整示例。请注意,它似乎是非确定性的:错误可能会出现,也可能不会出现!

答案#1

弄清楚哪些类型可以赋值给可索引类型以及从可索引类型赋值确实很令人困惑。原因是

f({ id: 123, name: "noname" }); // okay

之所以有效,是因为参数是一个对象字面量,它被赋予了一个隐式的索引签名。在发生此情况时,编译器知道该参数是一个对象字面量,因此它上面没有任何未知属性。因此,将其视为可分配给 AnyObjectWithNumberAndStringValues 的类型是安全的。

原因是

const o: ISupplier = { id: 123, name: "noname" };
f(o); // error

不能工作的原因是 o 被赋予了 ISupplier 类型注解。当 f(o) 被调用时,编译器已经忘记了 o 内部的具体属性。对编译器而言,里面可能存在未知的非 string 和非 number 的属性。

考虑以下代码:

interface ISupriser extends ISupplier {
  surprise: boolean;
}
const s: ISupriser = { id: 123, name: "noname", surprise: true };
const uhOh: ISupplier = s;
f(uhOh); // error makes sense now

一个 ISurpriser 包含一个 boolean 属性,所以你不会想用它来调用 f()。但是每个 ISurpriser 也是一个 ISupplier。变量 uhOh 和你的 o 一样,是有效的 ISupplier。编译器对 o 和 uhOh 的记忆只是它们是 ISupplier 的实例。这就是为什么 f(o) 会失败……这是为了防止你不小心调用 f(uhOh)。

请注意,任何允许你在不是对象字面量的东西上调用 f() 或类似函数的变通方法,都有可能引入编译器不知道(或不记得)的意外参数和不良属性。但无论如何,我们还是来看一下这些变通方法:

有人注意到,一个解决方法是使用 ISupplier 的类型别名而不是接口。显然,这里的权衡在于类型别名不如接口容易扩展(你可以通过交叉类型获得相同的效果),所以将其视为具有隐式索引签名是“足够安全的”:

type TSupplier = {
  id: number;
  name: string;
};
const p: TSupplier = { id: 123, name: "noname" };
f(p); // okay

添加不希望的属性可能不像使用接口那样“容易”,但仍然足够容易:

const ohNo: TSupplier = uhOh; // okay
f(ohNo); // no error, but there probably should be one!

另一个解决方法,尤其是在你无法更改 ISupplier 的情况下,是在你的函数中使用受约束的泛型类型,而不是索引签名,如下所示:

function g<T extends Record<keyof T, number | string>>(x: T) {
  console.log(x);
}

g() 函数基本上只接受其已知属性可分配给 number | string 的参数:

g({ id: 123, name: "noname" }); // okay
g(o); // okay
g(s); // error, "surprise" is incompatible

不过,同样地,如果编译器不知道某个属性,它就无法检查它,因此这些也没有错误:

g(uhOh); // no error, but there probably should be one
g(ohNo); // no error, but there probably should be one

无论如何,希望这能帮到你。祝你好运!

代码链接