如何使用“ in”和/或“ hasOwnProperty”缩小对象类型

时间:2020-09-10 16:28:47

标签: javascript typescript ecmascript-6

我正在为接口编写类型防护,并且注意到我无法用objectinObject.hasOwnProperty.call缩小arg.hasOwnProperty的范围。像这样:

interface Test {
    quest: string;
}

function isTest(arg: unknown): arg is Test {
    // this successfully narrows arg to "object"
    if (typeof(arg) !== "object" || arg === null) {
        return false;
    }

    if (!Object.hasOwnProperty.call(arg, "quest")) {
        return false;
    }
    // at this point, I still cannot access arg.quest, because
    // "Property 'quest' does not exist on type 'object'". The same thing happens
    // if I invert the check and try to access it inside the conditional.

    if (!arg.hasOwnProperty("quest")) {
        return false;
    }
    // same problem, cannot access arg.quest because arg is still just "object"

    if (!("quest" in arg)) {
        return false;
    }
    // still the same problem.

    return true;
}

因此,我不得不进行很多类型转换,这很烦人,因为我要检查的实际接口具有许多属性,而并非所有都是基本原语。因此,基本上,对于每个属性,我都必须做出类似以下的内容:

function isTest(arg: unknown): arg is Test {
    if (typeof(arg) !== "object" || arg === null) {
        return false;
    }

    if (!Object.hasOwnProperty.call(arg, "quest")) {
        return false;
    }
    if (typeof((arg as {quest: unknown}).quest) !== "string") {
        return false;
    }

    return true;
}

...而且通常,我觉得如果必须使用as,我做错了什么。那么如何在不为接口的每个属性编写单独的类型保护的情况下缩小object到接口的范围?

为什么我需要这样的强大防护,这是因为这要在服务器中进行,并且我正在检查JSON.parse之后发送到其API的有效负载,以便我可以对它们进行复杂的操作而不必担心找出其中一半的数据结构不良。

1 个答案:

答案 0 :(得分:2)

尽管any通常是不安全的,但我发现它作为user-defined type guard function的参数类型很有用:

function isTest(arg: any): arg is Test {
  return (typeof arg === "object") && (arg !== null) &&
    ("quest" in arg) && (typeof arg.quest === "string");
}

上面的编译没有错误。不,实际上并没有在实现中强制执行安全性,因为any有效地关闭了类型检查。但是只要您验证实现在做正确的事情,使用any就能使编译器摆脱束缚,以便isTest() callers 具有类型安全性

这与使用unknowntype assertions本质上是相同的,但是您说的话会让您感觉自己做错了事。

但是请记住,无论如何,用户定义的类型保护功能的实现都是以这种方式工作的。编译器为您验证的唯一一件事是您的返回类型为boolean。当您说这样的boolean结果可以视为由类型谓词arg is Test表示的类型防护时,它只是相信您。您可以编写此代码,例如:

function isBadTest(arg: unknown): arg is Test {
  return Math.random() < 0.5; // no error
}

因此,尽管您希望从编译器中获得尽可能多的类型安全性,但您应该记住,实现用户定义的类型保护可以将一些负担转移给您。在混合中添加asany并不会带来太多损失。


话虽这么说,让我忽略了以下事实:您正在编写自己的类型保护函数,而仅关注具有类型unknown的值并且您试图获取编译器的问题查看是否可以缩小到Test,或者至少可以缩小到具有quest属性的内容。

这里的问题是TypeScript只是没有内置的类型保护程序来将属性添加到对象类型的已知类型中。如前所述,您目前无法使用in来进行操作(请参阅microsoft/TypeScript#21732),也不能使用hasOwnProperty()来进行操作(请参见microsoft/TypeScript#18282)。如果需要这样的类型保护,则必须编写自己的类型。例如:

function hasProp<T extends object, K extends PropertyKey>(
  obj: T, 
  key: K
): obj is T & Record<K, any> {
  return key in obj;
}

function isTest2(arg: unknown): arg is Test {
  if (typeof arg !== "object") return false;
  if (arg === null) return false;
  if (!hasProp(arg, "quest")) return false; // use it here,
  if (typeof arg.quest !== "string") return false; // no error here
  return true;
}

但是,一旦开始沿用此路线,并牢记您的最终目的是在运行时验证反序列化的对象,以确保它们符合TypeScript接口,那么我最后得出结论,您会想要{{3 }}。在这里,我编写了一个迷你模式验证器,该验证器从较简单的类型保护器构建了复杂的类型保护器。使用该代码,您的isTest()就是:

const isTest3: G.Guard<Test> = G.gObject({ quest: G.gString });

其中G.gObject()带有属性类型保护对象,并为具有这些属性的对象产生类型保护,而G.gString是类型保护对象,它验证其参数为string

因此,您可能希望检查其他答案,以避免不得不为每个接口重新实现类型保护。

this answer