我有一个经典的接口来定义路由:
interface Routes {
[key: string]: string | Routes;
}
我喜欢为每个模块定义子接口,例如:
interface GeoRoutes extends Routes {
country: {
list: string;
};
}
到目前为止一切顺利,但我想防止GeoRoutes
对象有其他未定义的键。例如,声明this,编译:
const apiRoutes: GeoRoutes = {
foo: 'bar',
country: {
list: '/countries',
},
};
但我希望不是。
对我来说,扩展父接口的全部意义在于在工作于路由的方法中使用它,例如:
function addRoute(routes: Routes) { ... }
1条答案
按热度按时间gpfsuwkq1#
这更像是你想对
Routes
进行 * check *GeoRoutes
而不是 * extend * 它。有点像satisfies
操作符,但是是针对类型的。我倾向于创建一个名为SatisfiesRoutes<T>
的辅助类型,它不改变它的类型,但是如果T
是一个无效的Routes
类型,它会发出抱怨。可能是这样的:ToRoutes<T>
类型获取一个类型,并通过递归地将属性Map到string
或ToRoutes<>
来将其转换为有效的Routes
。其思想是如果T extends ToRoutes<T>
,则T
是有效的Routes
;否则,T extends ToRoutes<T>
的故障将给出关于T
的错误的错误信息。注意,我很想写
type SatisfiesRoutes<T extends ToRoutes<T>> = T
,但是编译器认为这是非法循环。由于不支持microsoft/TypeScript#51011中要求的任意循环约束,我需要解决它。通过使用ToRoutes<T> extends infer U ? U : never
中的条件类型推理,我延迟了足够长的时间来避免循环检测。好吧,我们来测试一下:
所以它允许好的类型,不允许坏的类型,并且因为
SatisfiesRoutes
只返回它的参数,GeoRoutes
类型没有索引签名,因此对象常量将根据需要进行额外的属性检查:在您实际上不想担心索引签名的情况下,也可以使用类似
T extends ToRoutes<T>
约束的行为来验证/无效潜在的Routes
类型,如下所示:Playground代码链接