typescript - 如何告诉 TypeScript 接口 T 比具有索引签名的类型 U 窄?
问题描述
我有一个函数可以验证 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[];
现在我有一个函数可以在给定要验证的对象和一个具有 shape 的模拟对象的情况下进行验证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),隐式索引签名仅适用于对象文字和type
别名,而不适用于interface
或class
类型。目前是设计使然;在没有确切类型的情况下推断隐式索引签名不是类型安全的。例如,Response
不知道类型值仅具有Data
属性。它可能具有与AnyJson
(例如,interface FunkyResponse extends Response { otherProp: ()=>void }
)不兼容的属性,因此编译器拒绝在那里推断隐式索引签名。这样做在技术上是不安全的type
别名也是如此,但无论出于何种原因,一个是允许的,另一个是不允许的。如果你想看到这种变化,你可能想去那个问题,给它一个和/或描述你的用例,如果你认为它很有吸引力。实际上看起来有人已经提到了这个用例。
所以,除非这个问题得到解决,否则我们能做什么?通常在这些情况下,我发现将我想要的类型表示为通用约束而不是具体类型更容易。索引签名改为映射类型。目标是提出一个泛型类型别名JsonConstraint<T>
,以便将有效的 JSON 类型 likeResponse
分配给,但将JsonConstraint<Response>
无效的 JSON 类型 like分配给。这是我可能写它的一种方式:Date
JsonConstraint<Date>
type JsonConstraint<T> = boolean | number | string | null | (
T extends Function ? never :
T extends object ? { [K in keyof T]: JsonConstraint<T[K]> }
: never
)
如果是可接受的原始类型之一,T extends JsonConstraint<T>
则为真,如果是函数,则为假,否则它会递归到每个属性并检查每个属性。这种递归应该适用于对象和数组,因为TypeScript 3.1 引入了映射的元组/数组类型。T
T
T
现在我想编写函数签名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
这将从推断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.
或者一个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
看起来不错。好的,希望有帮助;祝你好运!
推荐阅读
- c - C-服务器/客户端,等待另一个客户端然后退出
- javascript - HTML href 引用 JavaScript 变量 url
- jenkins - 如何在詹金斯中添加 -Djdk.util.zip.ensureTrailingSlash=false
- prolog - 当我使用 swi-prolog 中的安装程序来安装 aleph 时,我得到了 ERROR: source_sink `path(git)' does not exist
- php - 覆盖供应商自动加载作曲家
- spring-boot - 如何在 Spring Boot 中正确注册 HiddenHttpMethodFilter?
- javascript - 如何使用 selenium 检查会话存储中的条目
- sql-server - 多个枢轴
- matlab - 如何在 MATLAB 上用“findpeaks”确定边界上的最大值?
- php - 处理 SRP Auth 和生成设备密钥(PHP - 服务器端)