【问题标题】:Keyof nested child objectsKeyof 嵌套子对象
【发布时间】:2018-02-08 19:04:31
【问题描述】:

我有一个递归类型的对象,我想获取它的键和某种类型的任何子键。

例如。下面我想得到一个联合类型:

'/another' | '/parent' | '/child'

例子:

export interface RouteEntry {
    readonly name: string,
    readonly nested : RouteList | null
}
export interface RouteList {
    readonly [key : string] : RouteEntry
}

export const list : RouteList = {
    '/parent': {
        name: 'parentTitle',
        nested: {
            '/child': {
                name: 'child',
                nested: null,
            },
        },
    },
    '/another': {
        name: 'anotherTitle',
        nested: null
    },
}

在打字稿中,您可以使用 keyof typeof RouteList 来获取联合类型:

'/another' | '/parent' 

是否有方法也包含嵌套类型

【问题讨论】:

    标签: typescript


    【解决方案1】:

    这是一个艰难的过程。 TypeScript 缺少 mapped conditional types 通用的recursive type definitions,这两个都是我想用来为您提供联合类型的东西。 (编辑 2019-04-05:conditional types 在 TS2.8 中引入)您想要的有一些症结所在:

    • RouteEntrynested 属性有时可以是null,并且键入计算结果为keyof nullnull[keyof null] 的表达式开始破坏事物。一个需要小心。我的解决方法是添加一个虚拟密钥,使其永远不会为空,然后在最后将其删除。
    • 无论你使用什么类型的别名(称之为RouteListNestedKeys<X>)似乎都需要根据自身来定义,你会得到一个“循环引用”的错误。一种解决方法是提供可以工作到某个有限嵌套级别(例如,9 层深)的东西。这可能会导致编译器变慢,因为它可以急切地评估所有 9 个级别,而不是将评估推迟到以后。
    • 这需要大量涉及映射类型的类型别名组合,并且有一个 bug 组合映射类型,直到 TypeScript 2.6 才会修复。 workaround 涉及使用通用默认类型参数。
    • “在末尾删除一个虚拟键”步骤涉及一个名为 Diff 的类型操作,它至少需要 TypeScript 2.4 才能正常运行。

    这意味着:我有一个可行的解决方案,但我警告你,它既复杂又疯狂。在我输入代码之前的最后一件事:你需要改变

    export const list: RouteList = { // ...
    

    export const list = { // ...
    

    即从list变量中去掉类型注解。如果您将其指定为 RouteList,您将抛弃 TypeScript 对 list 的确切结构的了解,您将只得到 string 作为键类型。通过省略注释,您可以让 TypeScript 推断类型,因此它会记住整个嵌套结构。

    好的,开始吧:

    type EmptyRouteList = {[K in 'remove_this_value']: RouteEntry};
    type ValueOf<T> = T[keyof T];
    type Diff<T extends string, U extends string> = ({[K in T]: K} &
      {[K in U]: never} & { [K: string]: never })[T];
    type N0<X extends RouteList> = keyof X
    type N1<X extends RouteList, Y = {[K in keyof X]: N0<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
    type N2<X extends RouteList, Y = {[K in keyof X]: N1<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
    type N3<X extends RouteList, Y = {[K in keyof X]: N2<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
    type N4<X extends RouteList, Y = {[K in keyof X]: N3<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
    type N5<X extends RouteList, Y = {[K in keyof X]: N4<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
    type N6<X extends RouteList, Y = {[K in keyof X]: N5<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
    type N7<X extends RouteList, Y = {[K in keyof X]: N6<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
    type N8<X extends RouteList, Y = {[K in keyof X]: N7<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
    type N9<X extends RouteList, Y = {[K in keyof X]: N8<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y> 
    type RouteListNestedKeys<X extends RouteList, Y = Diff<N9<X>,'remove_this_value'>> = Y;
    

    让我们试试吧:

    export const list = {
        '/parent': {
            name: 'parentTitle',
            nested: {
                '/child': {
                    name: 'child',
                    nested: null,
                },
            },
        },
        '/another': {
            name: 'anotherTitle',
            nested: null
        },
    }
    
    type ListNestedKeys = RouteListNestedKeys<typeof list> 
    

    如果您检查ListNestedKeys,您将看到它是"parent" | "another" | "child",正如您所愿。值不值,看你自己了。

    哇!希望有帮助。祝你好运!

    【讨论】:

    • 一个小细节:如果您从RouteList 中删除索引签名readonly [key : string] : RouteEntry,这一切仍然有效。显然,当您提供实际的泛型参数为typeof list 时,索引签名永远不会用于类型推断。这使您可以在任何地方写extends {} 而不是extends RouteList,并从答案中完全删除RouteList 接口。
    【解决方案2】:

    这是一个无限递归的解决方案:

    type Paths<T> = T extends RouteList
      ? keyof T | { [K in keyof T]: Paths<T[K]['nested']> }[keyof T]
      : never
    
    type ListPaths = Paths<typeof list> // -> "/parent" | "/another" | "/child"
    

    在 Typescript v3.5.1 上测试。您还需要按照@jcalz 的建议从list 变量中删除类型注释。

    【讨论】:

    • 我喜欢这个答案(最近可能已经给出了一些版本),但我希望我知道这样的事情得到了官方的支持。在类型中递归地将属性下推一级是安全的,因为此类类型是惰性求值的,但递归索引对象(将属性上一级)可能是个问题,因为此类类型是急切求值的。
    • 这个答案真的很简洁
    • 它有效,但使用起来很危险,并且可能有一天会被删除,正如 TypeScript 问题中某处提到的那样。
    • @DanielSteigerwald:您能否指出打字稿问题中提到的地方,或者只是给出一些关于问题可能是什么的提示?
    【解决方案3】:

    @Aleksi 的回答展示了如何获取请求的类型联合"/parent" | "/another" | "/child"

    由于问题涉及路由层次结构,我想补充一点,从 typescript 4.1 开始,Template Literal Types 功能不仅可用于获取所有键,甚至可以生成所有可能的路由:@ 987654323@

    type ValueOf<T> = T[keyof T]
    type AllPaths<T extends RouteList, Path extends string = ''> = ValueOf<{
        [K in keyof T & string]: 
            T[K]['nested'] extends null 
            ? K 
            : (`${Path}${K}` | `${Path}${K}${AllPaths<Extract<T[K]['nested'], RouteList>>}`)
    }>
    
    type AllRoutes = AllPaths<typeof list> // -> "/parent" | "/another" | "/parent/child"
    

    与其他答案一样,list 对象可能没有 RouteList 类型注释,因为它会删除我们的 AllPaths 类型操作的类型信息。

    【讨论】:

      猜你喜欢
      • 2018-01-06
      • 2014-06-15
      • 1970-01-01
      • 1970-01-01
      • 2011-05-07
      • 1970-01-01
      • 2018-10-12
      • 2017-12-07
      • 1970-01-01
      相关资源
      最近更新 更多