TypeScript:在注解对象中推断类型的泛型。

问题描述 投票:0回答:1

我有一个通用类型叫 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',
    },
};

对象内部的每个值是 允许有自己的类型 对于 ParamsRouteConfig.

我的问题是:里面的 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?
  • 无意外错误
  • 类型安全(预期错误)

有什么办法可以实现以上所有的功能吗?

TS游乐场

typescript generics type-inference
1个回答
1
投票

要同时得到这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' });
}

游乐场链接


1
投票

首先,我们要说明的是 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',
    },
};

现在改变任何一个组件的参数类型都会导致错误,不需要投掷,也不会牺牲类型安全。

© www.soinside.com 2019 - 2024. All rights reserved.