问题描述
注意:对于下面显示的所有代码,使用 tsc --strict
调用 TypeScript。
给定一个单例对象 o
:
const o = {
foo: 1,bar: 2,baz: 3,};
如果我有一个在编译时无法知道的字符串值(比如来自用户输入),我想安全地使用该字符串来索引 o
。我不想向 o
添加索引签名,因为它不是动态的或可扩展的——它总是有这三个键。
如果我尝试简单地使用字符串来索引 o
:
const input = prompt("Enter the key you'd like to access in `o`");
if (input) {
console.log(o[input]);
}
TypeScript 按预期报告此错误:
error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ foo: number; bar: number; baz: number; }'.
No index signature with a parameter of type 'string' was found on type '{ foo: number; bar: number; baz: number; }'.
console.log(o[input]);
~~~~~~~~
如果我尝试更改条件以在运行时验证 input
的值是 o
的键:
if (input && input in o) {
console.log(o[input]);
}
这不足以让 TypeScript 相信操作是安全的,编译器也会报同样的错误。
但是,如果我用相同的逻辑来检查 input
是否是 custom type predicate 中 o
的键,那么程序会编译并按预期工作:
function isKeyOfO(s: string): s is keyof typeof o {
return s in o;
}
if (input && isKeyOfO(input)) {
console.log(o[input]);
}
我的问题是:还有其他方法可以将 string
类型的值缩小为 keyof typeof o
类型的值吗?我希望有另一种更简洁的方法。我还对使用动态字符串索引对象的用例的通用解决方案感兴趣,这样我就不需要特定于 o
的类型谓词。
解决方法
我认为没有什么比编译器可以验证为类型安全更简洁的了。您可以手动枚举这样的可能性:
if (input === "foo" || input === "bar" || input === "baz") {
console.log(o[input]); // okay
}
但你会发现,试图减少冗余只会导致更多的错误:
if ((["foo","bar","baz"] as const).includes(input)) { } // error!
// -----------------------------------------> ~~~~~
// Argument of type 'string | null' is not assignable to parameter
// of type '"foo" | "bar" | "baz"'.
(请参阅 this question 了解更多信息,以及 microsoft/TypeScript#36275 为什么这甚至不能充当类型保护)。
所以我一般不建议这样做(但见下文)。
在 microsoft/TypeScript#43284 有一个公开的建议允许 k in o
作为 k
上的类型保护,除了当前支持它作为 o
上的类型保护之外{1}}(见 microsoft/TypeScript#10485)。如果实现了这一点,您的原始支票 (input && input in o)
就会正常工作,不会出错。
GitHub 中的问题目前已打开并标记为“等待更多反馈”;因此,如果您想在某个时候看到这种情况发生,您可能想去那里,给它一个 ?,如果您认为它特别引人注目,请描述您的用例。
就我个人而言,我认为最好的解决方案可能是您的用户定义的带有 s is keyof typeof o
类型谓词的类型保护函数,因为它明确告诉编译器和任何其他开发人员您打算 s in o
缩小 { {1}} 以这种方式。
请注意,因为对象类型是可扩展的,所以您的自定义类型保护和 microsoft/TypeScript#43284 建议的自动类型保护在技术上都是不合理的; s
类型的值可能具有未知的属性,因为 TypeScript 中的对象类型是可扩展或开放:
typeof o
在这里,编译器将 const p = {
foo: 1,bar: 2,baz: 3,qux: "howdy"
};
const q: typeof o = p; // no error,object types are extendible
function isKeyOfQ(s: string): s is keyof typeof q {
return s in q;
}
if (input && isKeyOfQ(input)) {
input // "foo" | "bar" | "baz"
console.log(q[input].toFixed(2)); // no compiler error
// but what if input === "qux"?
}
视为与 q
具有相同的类型。这是真的,尽管有一个名为 o
的额外属性。 qux
会将 isKeyOfQ(input)
错误地缩小为 input
...,因此编译器认为 "foo" | "bar" | "baz"
是安全的。但是由于 q[input].toFixed(2)
该属性值属于 q.qux
类型,而其他属性值属于 string
类型,因此存在潜在危险。
在实践中,这种不健全的行为并不引人注目;在某些intentionally unsound behaviors in TypeScript中,便利性和开发人员的工作效率被认为更重要。
但是您应该清楚自己在做什么,以便仅在对象出处已知的情况下才使用这种缩小范围;如果您从某个不受信任的来源获得 number
,您可能需要更可靠的声音...例如 q
或通过 input === "foo" || input === "bar" || input === "baz"
实现的其他用户定义的类型保护处理:
["foo","baz"].includes(input)
,
我相信这就是您所追求的方法...
const o = {
foo: 1,} as const;
function printRequestedKey<Lookup extends Readonly<object>>(lookup: Lookup){
const input = prompt("Enter the key you'd like to access");
if (input !== null && input in lookup) {
console.log(lookup[input as keyof typeof lookup]);
}
}
printRequestedKey(o);
根据 Aadmaa 的回答,它将 const
添加到您对 o
的定义中。它还引入了 Generic 绑定,因此如果您正在执行控制台日志记录以外的任何操作,则返回值可以是源对象的某种投影。 javascript 保护 input in lookup
以及 Readonly 对象的使用让我确信显式转换 input as keyof typeof lookup
不会引入运行时错误。我添加了一个 null 检查,因为 prompt 可以返回 null(也许通过键转义?)。
你的方法很好。如果你愿意,你可以做类似的事情
const o = {
foo: 1,} as const;
type KeyO = keyof typeof o;
const input = prompt("Enter the key you'd like to access in `o`");
if (input in Object.keys(o)) {
const typedInput = input as KeyO;
}