这是一个已知问题(参见 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