我有一个通用类型叫 RouteConfig
的对象中使用。routes
.
// Generic type
type RouteConfig<Params> = {
path: string;
component: (params: Params) => string;
};
type Route = 'Home' | 'User';
// Object
const routes: Record<Route, RouteConfig<unknown>> = {
Home: {
path: 'a',
// False positive (should not error)
component: (params: { foo: string }) => 'x',
},
User: {
path: 'a',
// False positive (should not error)
component: (params: { bar: string }) => 'z',
},
};
对象内部的每个值是 允许有自己的类型 对于 Params
阃 RouteConfig
.
我的问题是:里面的 routes
类型注解,我应该传递什么作为通用到 RouteConfig
?
我不能提供单一类型,因为每个对象值都可以有自己的类型。(联合将对所有对象值应用相同的联合类型,这不是我想要的。)
在上面的例子中,我使用了 unknown
然而,这将导致假阳性类型错误(见上面代码示例中的注释)。
我不能使用 any
因为这样一来,当从对象中读取时,我就失去了类型安全。
const routes: Record<Route, RouteConfig<any>> = {
Home: {
path: 'a',
// True negative
component: (params: { foo: string }) => 'x',
},
User: {
path: 'a',
// True negative
component: (params: { bar: string }) => 'z',
},
};
// True negative
routes.Home.component({ foo: 'abc' });
// False negative (should error)
routes.Home.component({ bar: 'abc' });
我可以把类型注解从 routes
:
const routes = {
Home: {
path: 'a',
// True negative
component: (params: { foo: string }) => 'x',
},
User: {
path: 'a',
// True negative
component: (params: { bar: string }) => 'z',
},
};
// True negative
routes.Home.component({ foo: 'abc' });
// True positive
routes.Home.component({ bar: 'abc' });
......但是我失去了诸如 "查找引用 "和 "重命名 "这样的工具,在类型和属性里面的 RouteConfig
. 此外,我将失去一些类型安全,因为TypeScript将不再能够检查对象是否包含所有所需的键(由下面的定义)。Route
type)。
我想我要找的是一种方法来为 routes
对象 除了 的泛型--泛型应该从对象定义中推断出来。
总而言之,我正在寻找一种能够实现以下所有功能的方法。
RouteConfig
变化是否适用于所有用途?RouteConfig
?有什么办法可以实现以上所有的功能吗?
要同时得到这3个元素并不是很直接。我的一个方法是使用一个额外的函数进行推理(捕获对象文字的实际类型),然后对输入和输出类型进行一些重新设计。我在输入中加入了 & Record<Route, RouteConfig<any>>
的参数类型,让ts知道输入值的类型是 RouteConfig<any>
(否则ts会在重命名时漏掉这些),我通过的输出类型基本上是一个身份类型,以确保对 RouteConfig
被保留在输出中(如果没有这个用法,网站会在重命名中被遗漏)。
type RouteConfig<Params> = {
path: string;
component: (params: Params) => string;
};
type Route = 'Home' | 'User';
type RouteHelper<T extends Record<Route, RouteConfig<any>>> = {
[P in keyof T] : RouteConfig<T[P] extends RouteConfig<infer P> ? P : never>
}
function createRoutes<T extends Record<Route, RouteConfig<any>>>(r: T & Record<Route, RouteConfig<any>>): RouteHelper<T>{
return r as any;
}
// [✅] Good dev UX
// [✅] No unexpected errors
// [✅] Type safety (errors when expected)
{
const routes = createRoutes({
Home: {
path: 'a',
// True negative
component: (params: { foo: string }) => 'x',
},
User: {
path: 'a',
// True negative
component: (params: { bar: string }) => 'z',
},
});
// True negative
routes.Home.component({ foo: 'abc' });
// True positive
routes.Home.component({ bar: 'abc' });
}
首先,我们要说明的是 Home
应该有参数 {foo: String}
和 User
本该 {bar: string}
所以我们需要一个从这些路由到它们的参数类型的映射。我们可以使用一个对象类型来实现。
type ParamTypes = {
Home: {foo: string},
User: {bar: string}
};
现在我们要表达的是,对于每个路由, routes
应该有一个成员为该路线的 RouteConfig[T]
哪儿 T
是一种 ParamTypes.TheRoute
. 我们可以用这样的索引类型来表达。
{ [K in Route]: RouteConfig<ParamTypes[K]> }
所以定义 routes
成为
const routes: { [K in Route]: RouteConfig<ParamTypes[K]> } = {
Home: {
path: 'a',
// False positive (should not error)
component: (params: { foo: string }) => 'x',
},
User: {
path: 'a',
// False positive (should not error)
component: (params: { bar: string }) => 'z',
},
};
现在改变任何一个组件的参数类型都会导致错误,不需要投掷,也不会牺牲类型安全。