首页 > 解决方案 > 如何通知 Typescript 编译器已检查并定义了对象的每个值?

问题描述

我有以下对象,其值是动态检索的。每个函数调用(getFoogetBar等)都可以返回一个值或undefined.

let ExampleObj = {
  foo: getFoo(),
  bar: getBar(),
  baz: getBaz(),
  ...
} 

const getFoo:()=>Foo|undefined = () => {...}
...

根据对这个问题的回答here,我Array.every用来检查对象中的每个值是否已定义,如果甚至有一个未定义的值,则返回未定义,即 -

type ExampleObj = {
   foo: Foo
   bar: Bar
   baz: Baz
}

const returnsExampleObj:ExampleObj|undefined = () => {
  let exampleObj = {
    foo: getFoo(),
    bar: getBar(),
    baz: getBaz(),
  } 
  return Object.values(exampleObj).every(val => val != undefined) ? exampleObj : undefined;
};

但是,Typescript linter 无法推断我已经执行了此检查,因此它抱怨我返回的类型无效(因为 ExampleObj 值永远不能未定义)。我只需要这样做//@ts-ignore吗,还是有更好的方法来解决这个问题?

标签: typescript

解决方案


编译器将无法查看您的实现并得出exampleObj没有undefined属性的结论。

虽然您作为人类理解类型保护出来的数组 Object.values(obj)对 的类型有影响obj,但没有办法在 TypeScript 中表达这种关系。一般来说,TypeScript 中的一个值的类型保护只能对该值本身的表观类型产生影响(或者,如果您正在检查类型为可 区分联合的对象的判别属性,它可能会影响该对象的明显类型)。虽然能够通过其他操作传播类型保护会很好,但如果您尝试实际实现它,它将对编译器性能产生毁灭性影响。如对microsoft/TypeScript#12185的评论中所述, 类似功能的请求,

这将要求我们跟踪一个变量的特定值对其他变量的影响,这将增加控制流分析器的大量复杂性(以及相关的性能损失)。

因此,如果编译器自己无法弄清楚,我们必须告诉它。


如果您只打算Object.values(obj).every(...)在代码库中编写一次测试,那么您能做的最好的事情就是使用类型断言

const returnsExampleObjAssert = (): ExampleObj | undefined => {
    let exampleObj = {
        foo: getFoo(),
        bar: getBar(),
        baz: getBaz(),
    }
    return Object.values(exampleObj).every(val => val != undefined) ?
        exampleObj as ExampleObj : undefined;
};

通过编写exampleObj as ExampleObj,我们是在对编译器说“请把exampleObj它当作 type 的值来对待ExampleObj”。编译器只是相信你,因为它无法以一种或另一种方式找出真相。所以要小心不要对编译器撒谎(例如,`Object.values(exampleObj).some(val => val != undefined) ? exampleObj as ExampleObj : undefined)。


如果您可能对不同的对象多次执行此测试,那么编写一个用户定义的类型保护函数,其返回类型是表单的类型谓词arg is Type可能是有意义的。当您调用这样的函数时,编译器将理解true结果意味着arg可以缩小到Type,并且false结果意味着不能发生这种缩小(有时可能会发生不包括 Type的不同缩小)。这是我可以为您的测试做的方法:

function allPropsDefined<T extends object>(
    obj: T
): obj is { [K in keyof T]: Exclude<T[K], undefined> } {
    return Object.values(obj).every(v => typeof v !== "undefined");
}

该函数allPropsDefined()接受一个名为obj通用对象类型的参数T。实现返回一个boolean值;要么定义true了所有obj的属性,要么false不正确。返回类型obj is { [K in keyof T]: Exclude<T[K], undefined> }是一个类型谓词,可分配给boolean。该类型{ [K in keyof T]: Exclude<T[K], undefined> }映射类型。它具有与 相同的键T,但修改了属性;对于每个属性键K,该键的属性类型T[K], 已通过实用程序类型从其中undefined排除。所以如果是,那么是。ExcludeT[K]string | number | undefinedExclude<T[K], undefined>string | number

让我们测试一下:

const returnsExampleObjTypePredFunc = (): ExampleObj | undefined => {
    let exampleObj = {
        foo: getFoo(),
        bar: getBar(),
        baz: getBaz(),
    }

    return allPropsDefined(exampleObj) ?
        exampleObj // let exampleObj: { foo: Foo; bar: Bar; baz: Baz; }
        : undefined;
};

现在编译没有错误。您可以看到,在三元条件运算符的 true 子句中,exampleObj已从{foo: Foo | undefined, bar: Bar | undefined, baz: Baz | undefined}to缩小{foo: Foo, bar: Bar, baz: Baz},可以根据需要分配 to ExampleObj

同样,如果您只进行一次甚至两次测试,类型谓词函数的开销可能不值得。但是,如果您可能在代码库中多次执行此操作,则类型谓词函数可能会收回成本。

同样,编译器无法验证您的类型保护功能是否正确实现。我可以更改every()some(),编译器也会很高兴。编译器真正能检查的是返回类型是否匹配boolean。所以我们还是要小心不要对编译器撒谎。


Playground 代码链接


推荐阅读