如何告诉 TypeScript 接口 T 比具有索引签名的类型 U 窄?

2024-03-20

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

以下是我的类型,定义了所有可能的 JSON 值 – 取自https://github.com/microsoft/TypeScript/issues/1897#issuecomment-338650717 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'.

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


这是一个已知问题(参见 microsoft/TypeScript#15300) https://github.com/microsoft/TypeScript/issues/15300 that 隐式索引签名 https://www.typescriptlang.org/docs/handbook/release-notes/overview.html#implicit-index-signatures仅针对对象文字进行推断type别名,而不是为了interface or class类型。它是目前按设计 https://github.com/microsoft/TypeScript/issues/15300#issuecomment-332366024;在没有的情况下推断隐式索引签名确切的类型 https://github.com/microsoft/TypeScript/issues/12936不是类型安全的。例如,类型的值Response不知道only have a Data财产。它可能具有不兼容的属性AnyJson (e.g., interface FunkyResponse extends Response { otherProp: ()=>void })因此编译器拒绝在那里推断隐式索引签名。这样做在技术上是不安全的type别名也是如此,但无论出于何种原因,其中一个是允许的,而另一个则不允许。如果您想看到这一变化,您可能需要转到该问题并给它一个????和/或描述您的用例(如果您认为它引人注目)。实际上它看起来像有人已经提到过这个用例 https://github.com/microsoft/TypeScript/issues/15300#issuecomment-460226926.


那么,除非这个问题得到解决,否则我们能做什么?一般来说,在这些情况下,我发现将我想要的类型表示为通用约束 http://www.typescriptlang.org/docs/handbook/generics.html#generic-constraints而不是具体类型。索引签名被替换为映射类型。目标是提出一个泛型类型别名JsonConstraint<T>这样一个有效的 JSON 类型就像Response将可分配给JsonConstraint<Response>,但是无效的 JSON 类型,例如Date will not可分配给JsonConstraint<Date>。这是我可能会写的一种方式:

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

So T extends JsonConstraint<T>为真,如果T是可接受的原始类型之一,如果T是一个函数,否则它会递归到以下属性T并检查每一项。这种递归应该适用于对象和数组,因为TypeScript 3.1 引入了映射元组/数组类型 http://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html#mapped-types-on-tuples-and-arrays.

现在我想写函数签名isValid<T extends JsonConstraint<T>>(obj: AnyJson, shape: T): obj is AnyJson & T,但这是一个不可接受的循环约束。有时会发生这种情况。解决此问题的一种方法是将签名更改为isValid<T>(obj: AnyJson, shape: T & JsonConstraint<T>): obj is AnyJson & T。这将推断T from shape,然后检查JsonConstraint<T>仍然可以分配给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.

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

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

最后,我们应该能够使用string, number, boolean, null,以及这些的数组/对象没有错误:

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 代码链接 https://www.typescriptlang.org/play/#code/HYQwtgpgzgDiDGEAEApKB7YSDeSCwAUIUiUgC4CeMyAgsBWpkgLxIBG66ANhCFgD5JgAVzBsIAJySCoZCQEtgAc2lDhXLqsbAaEiSApaMwALIgYAbmKlK1VMbMwWOJAG0A1hAoAuJLIXKALq+dAzGSAC+VgSk5FTI2rr6hqyh2q6B0dYkimSSAGYIyAAqABaKKtjZsX5kwvn5vv4V0bER1bkFRUgAStAwmFDIVTE1JAAiIGQgvmUVGa2kUYTVtgnGAMKDciC5ADzFAHzOHNy8AmpikqrNyqoiGqoAFNWxxUgQAB55wAAmUEgAGLCYDwMjyJgAfiEEAAbtdvK9SO8vj9-kh0GwAFYQMFIaG4VwAaSQiiQngo6HySGKwXsmC2wH8u2AZAOxMCx3aozGvmAcMk1QAlCseUh8iCwRCsPIoAA1EBceS-A6HJ6YrEhejaAA0flK5ggsyQADJ6cBGcz9kchb4NaSAWsqRjsaaaTgkSQJBA6hIsA8uABCRYkbnVX64rggb1IeDbJC-KYzJBpYyi2IR+BRmNxplkJDe2CDI29frFkOk6lPWUKpW-J6J6Z6wsDJkQIVCj1ijNJgB0k2mvZ4yjIpQsSAA9BOMe4DPgxcsiGKa4rlQ2k3rcCCI-lFBBfgAFCToGC+bcQXf836RIXjqcfPToCSB+exe9jGoAP2-P9-f--AHVPexTxACzowMe1ASJQSAAOTnpe+5HiesFINGyCKHGYBwOCbA8OmpArnW65Ni4EYQDAXA+C4-KyPuvi4BBJ6SJQDHipKvhPJ2zDHAAjJEAkRDed7TpIx7PkB04gdQYHUkxUEwbBEqgqh6GkqC6DYVM8h4RAvYETk8qrvWjYgHq-IAO5IAOEBcbek6iY+ElisBoEYnJkEsYYsFkOgADKcgVKpMaYZpOE6Tw+lLrERFrqZm6erUEi+AARClOqJSIYC+LxGXdqQpy+GYo69vofyaVxSB7EgAAMvYAKx5R+ahcHy6hcE1H7Rslbi5UgABMeoAMyBJ1Ywamxya4GwviuClIDpeQEjCBA5ntYEgljcJDlCOgD7iaKh1LkdJ3HWdsRrCmzi4J8Z5-Bee7XouF3xEgABCzjaJaOz7DQhwdEZdZ7Ddd07o9kRqgGgZrRogYigQERAA

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何告诉 TypeScript 接口 T 比具有索引签名的类型 U 窄? 的相关文章

随机推荐