将字符串缩小为对象的键

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 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.

的类型谓词

你的方法不错。如果你愿意,你可以做类似

的事情
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 中的对象类型是 extendibleopen:

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"?
}

在这里,编译器认为 qo 具有相同的类型。这是事实,尽管有一个名为 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
}

Playground link to code