2017-08-30 134 views
5

我有一个递归类型的对象,我想获取某个类型的任意子键的键。嵌套子对象的键

例如,下面我想获得的联合类型:

'/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' 

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

回答

4

这是一个艰难的。 TypeScript缺少mapped conditional types和一般recursive type definitions,这两个都是我想用来给你的联合类型。有你想要的一些症结点:

  • 一个RouteEntrynested性能有时会null,并键入评估为keyof nullnull[keyof null]开始打破东西的表达。一个需要小心。我的解决方法包括添加一个虚拟键,使其不会为空,然后在最后删除它。
  • 无论您使用什么类型的别名(称为RouteListNestedKeys<X>)似乎都需要根据自身定义,并且您将收到“循环引用”错误。一种解决方法是提供一些适用于某些有限嵌套级别的东西(例如,深度为9级)。这可能会导致编译器放慢速度,因为它可能会急切地评估所有9个级别,而不是延迟评估,直到稍后。
  • 这需要大量的涉及映射类型的类型别名组合,并且有一个bug组合映射类型,直到TypeScript 2.6才会被修复。 A 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",因为你想要的。这取决于你是否值得。

Whe!希望有所帮助。祝你好运!

+0

一个小细节:如果您从RouteList中删除索引签名'readonly [key:string]:RouteEntry',这一切仍然有效。显然,当你提供实际泛型参数为'typeof list'时,索引签名从不用于类型推断。这使您可以在每个地方写入'extends {}'而不是'extends RouteList',并且完全从答案中移除'RouteList'接口。 – artem