首页 > 解决方案 > 处理丢失的对象键

问题描述

我试图找出使用 TypeScript 处理索引对象文字的最佳方法。

理想情况下,这是我想传递给类型检查器的信息......如果给出了已知的键,则期望该值是已知的,否则期望未定义。

这是一个具体的例子:

const MAPPING = { "A": 1, "B": 2 }

function lookup1(key: string) {
   return MAPPING[key] || 0
}

在这里,我们合理地得到以下错误:

元素隐式具有“任何”类型,因为“字符串”类型的表达式不能用于索引类型“{ A: number; B:号码;}'。

我认为以下函数可能有效,但 TypeScript 似乎无法正确推断类型:

function lookup2(key: string) {
    if (key in MAPPING) return MAPPING[key]
    return 0
}

第一个hacky解决方案

function lookup3(key: string): number {
    return MAPPING[key as keyof typeof MAPPING] || 0
}

问题:它很冗长,而且它依赖于类型检查器。如果你忘记了|| 0,一切看起来都很好。

第二个hacky解决方案

我可以更改映射的类型:

const MAPPING: { [i: string]: number | undefined } = { "A": 1, "B": 2 }

keyof typeof MAPPING但是,如果我需要保证密钥正确,现在我无法使用。

标签: typescript

解决方案


TypeScript 的类型系统并不完美并且有(很大程度上是故意的)漏洞,您可以在其中潜入不安全的事物。相反,有些地方会强制执行声音,但这种方式通常对需要使用类型断言或其他漏洞来解决它们的开发人员没有帮助。惯用的 TypeScript 通常是不安全的;通常只有以跳过看似不必要的障碍为代价才能恢复安全;安全有时是虚幻的。

我的观点:只要您意识到它们的局限性,您的“hacky”解决方案基本上就可以了。你有时可以重写这些断言,使它们不是谎言,但你并没有真正被阻止撒谎。只要您可以限制任何潜在谎言的范围,以便它们不会轻易传播到其他代码,那么您可能正在做一些合理的事情。


让我们看看其他一些可能的解决方案。以下内容可能与我想象的一样安全:

function lookup1(key: string) {
    return (key === "A" || key === "B") ? MAPPING[key] : 0; // no lies, but redundant
}

在这里,您明确比较key了文字字符串"A""B". 它确实涉及运行时的重复,这可能比使用类型断言更糟糕,但我想不出明显的方法来欺骗编译器。因此,您可以通过一些不太可扩展的箍跳来换取安全。


那你为什么不能检查一下key in MAPPING?这是因为 TypeScript 中的对象类型是开放的而不是精确的。TypeScript 知道MAPPING包含AB键。它不知道缺少所有其他键。就编译器而言,可能MAPPING具有键CD键,例如string

type MappingType = { A: number, B: number };
const weirdMapping = {A: 1, B: 2, C: "three", D: "four"};
const MAPPING: MappingType = weirdMapping;

因此,仅检查key in MAPPING并不意味着您已经检查了 if MAPPING[key]is of type number。并且出于类似的原因Object.keys(MAPPING)被认为是类型string[]而不是Array<"A" | "B">。这是编译器以一种在很多时候感觉无用的方式强制执行健全性的地方之一。

如果你想告诉编译器不要担心这个问题(并自己负责防止这些边缘情况),你可以使用这样的类型断言:

function lookup2(key: string) {
    return (key in MAPPING) ? MAPPING[key as keyof typeof MAPPING] : 0;
}

或者您可以创建一个用户定义的类型保护,它允许您向编译器断言boolean-returning 函数充当对其参数之一的检查:

function lookup3(key: string) {
    // user defined type guards are not safe either, much like type assertions
    function isKeyOf<T>(obj: T, k: PropertyKey): k is keyof T {
        return k in obj; 
    }
    return isKeyOf(MAPPING, key) ? MAPPING[key] : 0;
}

类型断言和用户定义的类型保护都允许你对编译器撒谎。只要我们确定MAPPING只包含AB键,那么我们实际上并没有撒谎。这是否比您在问题中的代码或多或少是主观的。


您可以做的另一件事是按照您的第二个解决方案的思路,但不是扩大MAPPING自身,而是创建一个新的更广泛的变量并分配MAPPING给它:

function lookup4(key: string) {
    const MAPPINGWIDE: { [k: string]: number | undefined } = MAPPING;
    return MAPPINGWIDE[key] || 0;
}

这里MAPPINGWIDE只存在于lookup函数内部,但我们已经告诉编译器它的所有属性都是numberor undefinedMAPPING编译器允许我们在没有任何断言或漏洞的情况下对其进行分配。如果您更改MAPPINGconst MAPPING = {A: 1, B: 2, oops: "hello"}编译器,则会在您的MAPPINGWIDE作业中抱怨。所以它只是有点多余(我们必须复制MAPPING),而且非常安全,对吧?

好吧,也许不是。请注意,如果您MAPPING通过weirdMapping上面定义,那么代码仍然可以编译而没有错误。您可以扩大weirdMappingtoMappingType然后分配MappingTypeto MAPPINGWIDE,即使这是不安全的。因此lookup4("C")string在运行时产生一个编译器认为是number. 代码lookup4("C").toFixed()将在运行时崩溃,没有编译器警告。

这是“有时是虚幻的”类型安全。这可能是一个问题吗?可能不是。但这个解决方案是否比其他解决方案更多或更少是一个判断电话。


好的,希望能给你一些方向,可能还有不同的视角。祝你好运!

Playground 代码链接


推荐阅读