typescript 如何推断一个深层对象的关键字类型与功能参数在TS?

sf6xfgos  于 2023-04-07  发布在  TypeScript
关注(0)|答案(1)|浏览(110)

我试图理解Typescript如何允许我推断路由对象的键。以下是我的相关类型:

type Route = {
    layout: (props: PropsWithChildren) => EmotionJSX.Element;
    prefix: string;
    routes: Record<Uppercase<string>, Page>;
}

const auth = {
    layout: AuthLayout,
    prefix: "/auth",
    routes: {
        LOGIN: {
            path: "/login",
            component: lazy(() => import("../pages/Login"))
        },
    }
} satisfies Route;

const user = {
    layout: UserLayout,
    prefix: "/user",
    routes: {
        DASHBOARD: {
            path: "/dashboard",
            component: lazy(() => import("../pages/User/Dashboard"))
        },
    }
} satisfies Route;

export { user, auth };

所以我正在使用一个getRoutePath()函数,它有两个参数:路由类型和路由名称。该函数可以将路由类型(user或auth)之一作为第一个参数,将路由属性的一个键作为第二个参数,并返回路由的路径。我已经成功地编写了这些函数,它工作正常,它推断了路由类型和路由对象的键。

import ROUTES from "./constants/routes"

const getRoutePath= <T extends keyof typeof ROUTES, R extends keyof typeof ROUTES[T]["routes"]>(type: T, name: R): string | false => {
    if (!ROUTES[type]) return false;
    if (!params) params = {};

    let path = "";
    if (ROUTES[type].prefix) path = ROUTES[type].prefix;

    if (ROUTES[type].routes[name].path) {
        ^^^^^^^^^^^^^^^^^^^^^^^^^ => This is throwing an error

        path += ROUTES[type].routes[name].path;
    }
    return path;
}

我可以成功地使用getRoutePath(“admin”,“DASHBOARD”),而不是getRoutePath(“admin”,“NOTHING”)。然而,ts说函数中有一个错误:类型R不能用于索引类型{ DASHBOARD:{ path:string;〈()=〉Element〉;联系我们|{ ...}|{ ...}
我不明白为什么TS在这里抛出一个错误。
我尝试过使用强类型路由而不是使用satisfies,但当我这样做时,我再也无法推断函数的第二个参数。我尝试过创建一个makeRoute函数来强制函数定义,但我仍然得到相同的错误。
这里是一个链接到一个可复制的例子:Typescriptland.orgplay

fjaof16o

fjaof16o1#

接下来我将使用

type Routes = typeof routes;

好方便讨论
问题是,当你用一个特定的键索引到一个generic类型的值时,编译器倾向于首先将泛型的类型扩展到它的约束(如在microsoft/TypeScript#33181的注解中提到的)。在getRoutePath()内部,这意味着虽然routes[type]被视为泛型类型Routes[T],但routes[type].routes被扩展到union类型:

const r = routes[type].routes;
/* const r: {
    LOGIN: {
        path: string;
        title: string;
    };
} | {
    DASHBOARD: {
        path: string;
        title: string;
    };
} | {
    DASHBOARD: {
        path: string;
        title: string;
    };
} */

此时编译器将无法跟踪该类型与name的类型R extends keyof Routes[T]["routes"]之间的相关性,并且不会让您索引routes[type].routes[name]
这类似于microsoft/TypeScript#30581报告的问题,其中有两个union类型的值,它们以始终兼容的方式相互关联,但编译器无法理解。
修复它的最佳方法取决于你的用例。如果你确信你做了正确的事情,只是想让编译器停止抱怨,你可以通过Assert来解决:

const r = routes[type].routes as Record<R, { path: string }>; 
if (r[name].path) {
    path += r[name].path;
}
  • 你 * 知道routes[type].routesR索引处有一个{path: string}兼容的值,所以你可以告诉编译器,然后继续。编译器不能 * 验证 * 这一点,所以你已经把一些类型安全的责任从编译器身上拿走了。

如果你不想这样做,并且希望编译器能够很好地遵循逻辑,以便在你出错时发出抱怨,那么你需要以类似于microsoft/TypeScript#47109中描述的方式进行重构,这是处理相关联合的推荐方法,如microsoft/TypeScript#30581中所述。
本质上,你需要重写你的类型,使它们显式地以泛型indexed accesses的形式变成mapped types。在你的例子中,它可能看起来像这样:

type RouteRoutes = { [T in keyof Routes]:
    keyof Routes[T]["routes"]
};
/* type RouteRoutes = {
  admin: "DASHBOARD";
  user: "DASHBOARD";
  auth: "LOGIN";
} */

type _Routes = { [T in keyof Routes]:
    { routes: Record<RouteRoutes[T], { path: string }> }
};
const _routes: _Routes = routes; // okay

在这里,我们创建了一个RouteRoutes类型,它是从Routes的键到routes属性的键的简单Map。然后_RoutesRoutes的一个版本,它将RouteRoutes中的关系显式捕获为Map类型。我们可以将routes赋值给Routes类型的变量_routes
然后我们重构getRoutePathR类型,将其约束为RouteRoutes[T]

const getRoutePath = <T extends keyof Routes, R extends RouteRoutes[T]>(
    type: T, name: R,
    params: Record<string, string> = {},
    query: Record<string, string> = {}
): string => { /* ✂ ⋯ ✂ */ }

现在,在函数内部,如果我们索引到_routes[type].routes而不是routes[type].routes,编译器将其视为R中的泛型:

const r: Record<R, { path: string }> = _routes[type].routes;

这意味着你可以用R索引它而不会出错:

if (_routes[type].routes[name].path) {
        path += _routes[type].routes[name].path;
    }

Playground链接到代码

相关问题