将字符串缩小为对象的键

问题描述

注意:对于下面显示的所有代码,使用 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 predicateo 的键,那么程序会编译并按预期工作:

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)

Playground link to code

,

我相信这就是您所追求的方法...

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;
  }

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...