将字符串缩小为对象的键
Narrowing a string to a key of an object
注意:对于下面显示的所有代码,使用 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
.
的类型谓词
你的方法不错。如果你愿意,你可以做类似
的事情
const o = {
foo: 1,
bar: 2,
baz: 3,
} 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;
}
我相信这就是您想要的方法...
const o = {
foo: 1,
bar: 2,
baz: 3,
} 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
的定义中。它还引入了一个通用绑定,这样如果您正在做控制台日志以外的任何事情,那么 returned 值可以是源对象的某种投影。 javascript 守卫 input in lookup
以及 Readonly 对象的使用让我相信显式转换 input as keyof typeof lookup
不会引入运行时错误。我添加了一个空检查,因为 prompt 可以 return null(也许通过键转义?)。
我认为没有比编译器可以验证为类型安全的更简洁的了。您可以手动枚举这样的可能性:
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"'.
(请参阅 for more info, and also microsoft/TypeScript#36275 了解为什么这甚至不能充当类型保护)。
所以我一般不建议这样做(但见下文)。
microsoft/TypeScript#43284 to allow k in o
to act as a type guard on k
, in addition to the current support for it to act as a type guard on o
(see microsoft/TypeScript#10485 处有一个开放的建议)。如果实施了,您的原始检查 (input && input in o)
将正常工作。
GitHub 中的问题当前未解决并标记为“等待更多反馈”;因此,如果您希望在某个时候看到这种情况发生,您可能想去那里,给它一个,如果您认为它特别引人注目,请描述您的用例。
我个人认为最好的解决方案可能是使用 s is keyof typeof o
类型谓词的用户定义类型保护函数,因为它明确告诉编译器和任何其他开发人员您打算 s in o
以这种方式缩小s
。
请注意,因为对象类型是 可扩展的 ,所以您的自定义类型保护和 microsoft/TypeScript#43284 中提议的自动类型保护在技术上都是不合理的; typeof o
类型的值很可能具有未知的属性,因为 TypeScript 中的对象类型是 extendible 或 open:
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
类型,而其他的是number
类型,所以有危险。
在实践中,这种不稳固的情况并不是什么大不了的事;在某些 intentionally unsound behaviors in TypeScript 中,便利性和开发人员生产力被认为更为重要。
但是你应该知道你在做什么,所以你只在你的对象的出处已知的情况下使用这种缩小;如果你从一些不受信任的来源获得 q
,你可能想要一些更可靠的东西......例如 input === "foo" || input === "bar" || input === "baz"
或通过 ["foo", "bar", "baz"].includes(input)
:[=39 实现的其他一些用户定义的类型防护处理=]
function isSafeKeyOfQ(s: string): s is keyof typeof q {
return ["foo", "bar", "baz"].includes(s);
}
if (input && isSafeKeyOfQ(input)) {
console.log(q[input].toFixed(2)); // safe
}
注意:对于下面显示的所有代码,使用 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
.
你的方法不错。如果你愿意,你可以做类似
的事情const o = {
foo: 1,
bar: 2,
baz: 3,
} 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;
}
我相信这就是您想要的方法...
const o = {
foo: 1,
bar: 2,
baz: 3,
} 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
的定义中。它还引入了一个通用绑定,这样如果您正在做控制台日志以外的任何事情,那么 returned 值可以是源对象的某种投影。 javascript 守卫 input in lookup
以及 Readonly 对象的使用让我相信显式转换 input as keyof typeof lookup
不会引入运行时错误。我添加了一个空检查,因为 prompt 可以 return null(也许通过键转义?)。
我认为没有比编译器可以验证为类型安全的更简洁的了。您可以手动枚举这样的可能性:
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"'.
(请参阅
所以我一般不建议这样做(但见下文)。
microsoft/TypeScript#43284 to allow k in o
to act as a type guard on k
, in addition to the current support for it to act as a type guard on o
(see microsoft/TypeScript#10485 处有一个开放的建议)。如果实施了,您的原始检查 (input && input in o)
将正常工作。
GitHub 中的问题当前未解决并标记为“等待更多反馈”;因此,如果您希望在某个时候看到这种情况发生,您可能想去那里,给它一个,如果您认为它特别引人注目,请描述您的用例。
我个人认为最好的解决方案可能是使用 s is keyof typeof o
类型谓词的用户定义类型保护函数,因为它明确告诉编译器和任何其他开发人员您打算 s in o
以这种方式缩小s
。
请注意,因为对象类型是 可扩展的 ,所以您的自定义类型保护和 microsoft/TypeScript#43284 中提议的自动类型保护在技术上都是不合理的; typeof o
类型的值很可能具有未知的属性,因为 TypeScript 中的对象类型是 extendible 或 open:
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
类型,而其他的是number
类型,所以有危险。
在实践中,这种不稳固的情况并不是什么大不了的事;在某些 intentionally unsound behaviors in TypeScript 中,便利性和开发人员生产力被认为更为重要。
但是你应该知道你在做什么,所以你只在你的对象的出处已知的情况下使用这种缩小;如果你从一些不受信任的来源获得 q
,你可能想要一些更可靠的东西......例如 input === "foo" || input === "bar" || input === "baz"
或通过 ["foo", "bar", "baz"].includes(input)
:[=39 实现的其他一些用户定义的类型防护处理=]
function isSafeKeyOfQ(s: string): s is keyof typeof q {
return ["foo", "bar", "baz"].includes(s);
}
if (input && isSafeKeyOfQ(input)) {
console.log(q[input].toFixed(2)); // safe
}