TypeScript中只读Record转换后的类型推断优化及问题解析
问题分析与解决方案
问题场景
定义了包含路由信息的ROUTES对象,每个路由必填path和title,可包含额外属性:
export type TRoute = { path: string; title: string; } & Record<string, any>; export type TRoutes = Record<string, TRoute>; export const ROUTES = { dashboard: { path: "/dashboard", title: "Dashboard" }, todos: { path: "/todos", title: "Todos" }, settings: { path: "/settings", title: "Settings", foo: 2 } } as const satisfies TRoutes;
编写getRoutesList函数将ROUTES转为数组:
type TValueOfRecord<T> = T[keyof T]; type TRecordOfRecord<T> = Record<string, TValueOfRecord<T>>; const getRoutesList = <T extends TRecordOfRecord<T> & TRoutes>(routes: T) => { return Object.values(routes); }; export const ROUTE_LIST = getRoutesList(ROUTES);
此时遍历ROUTE_LIST访问仅settings路由拥有的foo字段,TypeScript未报错:
ROUTE_LIST.map((route) => { console.log(route.foo); // <- 未报错 return route; });
原因分析
- 类型定义过于宽松:
TRoute中的& Record<string, any>意味着所有路由对象都允许任意字符串键的属性,TypeScript会默认认为每个路由都可能存在foo(或任何其他属性),因此访问时不会触发错误。 - 泛型约束冗余:
getRoutesList的泛型约束T extends TRecordOfRecord<T> & TRoutes完全没必要,再加上TRoute本身的宽松定义,返回的数组元素类型被放宽为所有路由属性的超集,进一步模糊了类型边界。
重构方案
步骤1:修正类型定义,保留精确路由类型
去掉TRoute中宽松的Record<string, any>,改用BaseRoute作为基础约束,同时让ROUTES保留自身的字面量类型:
// 基础路由约束,仅定义必填字段 type BaseRoute = { path: string; title: string; }; // 定义ROUTES,用satisfies确保每个路由符合BaseRoute,同时保留独有属性 export const ROUTES = { dashboard: { path: "/dashboard", title: "Dashboard" }, todos: { path: "/todos", title: "Todos" }, settings: { path: "/settings", title: "Settings", foo: 2 } } as const satisfies Record<string, BaseRoute>; // 自动推导路由类型 export type TRoutes = typeof ROUTES; export type TRoute = TRoutes[keyof TRoutes];
步骤2:简化getRoutesList函数
不需要复杂泛型约束,直接返回输入对象值的联合类型数组:
const getRoutesList = <T extends Record<string, BaseRoute>>(routes: T) => { return Object.values(routes) as (T[keyof T])[]; }; export const ROUTE_LIST = getRoutesList(ROUTES); // 此时ROUTE_LIST的类型为各路由字面量类型的联合数组,类型精确
步骤3:安全访问独有属性
现在TypeScript会准确识别每个路由的属性,访问不存在的属性会报错,需要通过类型守卫处理:
ROUTE_LIST.map((route) => { // 直接访问route.foo会报错:属性'foo'在部分路由类型上不存在 if ('foo' in route) { console.log(route.foo); // 类型守卫后可安全访问 } return route; });
内容的提问来源于stack exchange,提问作者singhAmandeep007
相关产品推荐
相关产品推荐

