【问题标题】:How do I tell TypeScript that interface T is narrower than type U which has an index signature?如何告诉 TypeScript 接口 T 比具有索引签名的类型 U 窄?
【发布时间】:2020-04-24 11:55:00
【问题描述】:

我有一个函数可以验证 JSON 响应以确保它对应于给定的形状。

这是我定义所有可能的 JSON 值的类型——取自 https://github.com/microsoft/TypeScript/issues/1897#issuecomment-338650717

type AnyJson = boolean | number | string | null | JsonArray | JsonMap;
type JsonMap = { [key: string]: AnyJson };
type JsonArray = AnyJson[];

现在我有一个函数可以在给定要验证的对象和形状为T 的模拟对象的情况下进行验证。

function isValid<T extends AnyJson>(obj: AnyJson, shape: T): obj is T {
  // ... implementation
}

但是,当我尝试使用接口和真实对象调用函数时,我在类型参数中的 Thing 下遇到类型错误

interface Response {
  Data: Thing[]; // Thing is an interface defined elsewhere
};

isValid<Response>(data, { Data: [] })
//      ^^^^^^^^
Type 'Response' does not satisfy the constraint 'AnyJson'.
  Type 'Response' is not assignable to type 'JsonMap'.
    Index signature is missing in type 'Response'.

奇怪的是,当Response 是类型而不是接口时,这种情况不会发生,例如

type Response = {
  Data: Thing[];
};

但我确实遇到了同样的错误,但在Thing 本身上,它仍然是一个接口:

Type 'Response' does not satisfy the constraint 'AnyJson'.
  Type 'Response' is not assignable to type 'JsonMap'.
    Property 'Data' is incompatible with index signature.
      Type 'Thing[]' is not assignable to type 'AnyJson'.
        Type 'Thing[]' is not assignable to type 'JsonArray'.
          Type 'Thing' is not assignable to type 'AnyJson'.
            Type 'Thing' is not assignable to type 'JsonMap'.
              Index signature is missing in type 'Thing'.

所以我的问题是,为什么这种预期的缩小不会发生在接口上,而只是发生在类型上?

【问题讨论】:

    标签: typescript


    【解决方案1】:

    (see microsoft/TypeScript#15300) 的一个已知问题是 implicit index signatures 仅适用于对象文字和 type 别名,而不适用于 interfaceclass 类型。这是currently by design;在没有 exact types 的情况下推断隐式索引签名不是类型安全的。例如,Response 类型的值只有 不知道具有 Data 属性。它可能具有与AnyJson 不兼容的属性(例如interface FunkyResponse extends Response { otherProp: ()=&gt;void }),因此编译器拒绝在那里推断隐式索引签名。对type 别名执行此操作在技术上也是不安全的,但无论出于何种原因,一个是允许的,另一个是不允许的。如果您想看到这种变化,您可能需要转到该问题并给它一个 ? 和/或如果您认为它令人信服,请描述您的用例。实际上它看起来像someone has mentioned this use case already


    那么,除非这个问题得到解决,否则我们能做什么?通常在这些情况下,我发现将我想要的类型表示为 generic constraint 而不是具体类型更容易。索引签名改为映射类型。目标是提出一个泛型类型别名 JsonConstraint&lt;T&gt;,以便将像 Response 这样的有效 JSON 类型分配给 JsonConstraint&lt;Response&gt;,但像 Date 这样的无效 JSON 类型将不会可分配给JsonConstraint&lt;Date&gt;。这是我可能写的一种方式:

    type JsonConstraint<T> = boolean | number | string | null | (
        T extends Function ? never :
        T extends object ? { [K in keyof T]: JsonConstraint<T[K]> }
        : never
    )
    

    所以如果T 是可接受的原始类型之一,则T extends JsonConstraint&lt;T&gt; 为真,如果T 是一个函数,则为假,否则它递归到T 的属性并检查每个属性。这种递归应该适用于对象和数组,因为TypeScript 3.1 introduced mapped tuple/array types

    现在我想编写函数签名isValid&lt;T extends JsonConstraint&lt;T&gt;&gt;(obj: AnyJson, shape: T): obj is AnyJson &amp; T,但这是一个不可接受的循环约束。它有时会发生。解决它的一种方法是将签名更改为isValid&lt;T&gt;(obj: AnyJson, shape: T &amp; JsonConstraint&lt;T&gt;): obj is AnyJson &amp; T。这将从shape 推断T,然后检查JsonConstraint&lt;T&gt; 是否仍可分配给shape。如果是这样,那就太好了。如果不是,则该错误应该提供信息。

    所以这里是isValid()

    function isValid<T>(obj: AnyJson, shape: T & JsonConstraint<T>): obj is typeof obj & T {
        return null!; // impl here
    }
    

    现在让我们测试一下:

    declare const data: AnyJson
    
    declare const response: Response;
    if (isValid(data, response)) {
        data.Data.length; // okay
    };
    

    这样就可以正常工作,如您所愿。让我们看看它对于其他类型的行为是否符合预期。我们不应该使用undefined 作为属性类型:

    isValid(data, { undefinedProp: undefined }); // error! 
    //            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Types of property 'undefinedProp' are incompatible
    

    或函数值属性:

    isValid(data, { deeply: { nested: { property: { func: () => 1 } } } }); // error!
    // Types of property 'func' are incompatible.
    

    或者Date(失败是因为它有各种不可序列化的方法):

    isValid(data, new Date()); // error!
    // Types of property 'toString' are incompatible.
    

    最后,我们应该能够正确使用stringnumberbooleannull 以及它们的数组/对象:

    isValid(data, {
        str: "",
        num: 1,
        boo: Math.random() < 0.5,
        nul: null,
        arr: [1, 2, 3],
        obj: { a: { b: ["a", true, null] } }
    }); // no error
    

    看起来不错。好的,希望有帮助;祝你好运!

    Playground link to code

    【讨论】:

      猜你喜欢
      • 2020-02-02
      • 2017-08-02
      • 2019-05-01
      • 2016-01-25
      • 1970-01-01
      • 2020-12-24
      • 2016-04-07
      • 2016-10-11
      • 2018-09-15
      相关资源
      最近更新 更多