将字符串缩小为对象的键

Jim*_*dra 10 typescript type-narrowing

tsc --strict注意:下面显示的所有代码都会调用 TypeScript 。

\n

给定一个单例对象o

\n
const o = {\n  foo: 1,\n  bar: 2,\n  baz: 3,\n};\n
Run Code Online (Sandbox Code Playgroud)\n

如果我有一个在编译时无法知道的字符串值(比如来自用户输入),我想安全地使用该字符串来索引o。我不想添加索引签名,o因为它不是动态的或可扩展的\xe2\x80\x94它总是具有这三个键。

\n

如果我尝试简单地使用字符串来索引o

\n
const input = prompt("Enter the key you\'d like to access in `o`");\n\nif (input) {\n  console.log(o[input]);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

正如预期的那样,TypeScript 报告此错误:

\n
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; }\'.\n  No index signature with a parameter of type \'string\' was found on type \'{ foo: number; bar: number; baz: number; }\'.\n\n     console.log(o[input]);\n                 ~~~~~~~~\n
Run Code Online (Sandbox Code Playgroud)\n

如果我尝试更改条件以在运行时验证 的值input是 的键o

\n
if (input && input in o) {\n  console.log(o[input]);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这不足以让 TypeScript 相信该操作是安全的,并且编译器会报告相同的错误。

\n

但是,如果我包装相同的逻辑来检查自定义类型谓词中是否input为 key ,则程序将按预期编译并工作:o

\n
function isKeyOfO(s: string): s is keyof typeof o {\n  return s in o;\n}\n\nif (input && isKeyOfO(input)) {\n  console.log(o[input]);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我的问题是:还有其他方法可以将 type 的值缩小string为 type 的值keyof typeof o吗?我希望有另一种更简洁的方法。我还对使用动态字符串索引对象的用例的通用解决方案感兴趣,这样我就不需要特定于 的类型谓词o

\n

jca*_*alz 6

我认为没有什么比编译器可以验证类型安全更简洁的了。您可以像这样手动枚举可能性:

if (input === "foo" || input === "bar" || input === "baz") {
    console.log(o[input]); // okay
}
Run Code Online (Sandbox Code Playgroud)

但你会发现试图减少冗余只会导致更多错误:

if ((["foo", "bar", "baz"] as const).includes(input)) { } // error!
// -----------------------------------------> ~~~~~
// Argument of type 'string | null' is not assignable to parameter
// of type '"foo" | "bar" | "baz"'.
Run Code Online (Sandbox Code Playgroud)

(请参阅此问题以获取更多信息,以及microsoft/TypeScript#36275以了解为什么这甚至不能充当类型保护)。

所以我一般不建议这样做(但见下文)。


microsoft/TypeScript#43284有一个公开建议,除了当前支持它充当类型k in o防护k之外o(请参阅microsoft/TypeScript#10485),还允许充当 的类型防护。如果实现了这一点,您的原始检查(input && input in o)将不会出现任何错误。

GitHub 中的问题目前已打开并标记为“等待更多反馈”;因此,如果您想在某个时候看到这种情况发生,您可能想去那里,给它一个 ,并描述您的用例(如果您认为它特别引人注目)。


我个人认为最好的解决方案可能是带有s is keyof typeof o类型谓词的用户定义的类型保护函数,因为它明确地告诉编译器和任何其他开发人员您打算s in o以这种方式缩小s范围。


请注意,由于对象类型是可扩展的,因此您的自定义类型保护和 microsoft/TypeScript#43284 提议的自动类型保护在技术上都是不健全的;type 的值typeof o很可能具有未知的属性,因为 TypeScript 中的对象类型是可扩展的开放的

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"?
}
Run Code Online (Sandbox Code Playgroud)

在这里,编译器认为q具有与 相同的类型o。这是事实,尽管有一个名为 的额外属性quxisKeyOfQ(input)将错误地缩小input"foo" | "bar" | "baz"...,因此编译器认为q[input].toFixed(2)是安全的。但由于q.qux该属性值的类型为 ,string而其他属性值的类型为number,因此存在潜在的危险。

实际上,这种不合理的做法并不是什么大事。TypeScript 中存在一些故意不合理的行为,其中便利性和开发人员生产力被认为更重要。

但是您应该意识到自己在做什么,以便仅在对象来源已知的情况下使用这种缩小范围;如果您q从某些不受信任的来源获得信息,您可能需要一些更可靠的东西...例如input === "foo" || input === "bar" || input === "baz"或通过以下方式实现的其他一些用户定义的类型保护处理["foo", "bar", "baz"].includes(input)

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
}
Run Code Online (Sandbox Code Playgroud)

Playground 代码链接