如何在TypeScript中从接口中提取“路径表达式”?

brccelvz  于 2023-02-25  发布在  TypeScript
关注(0)|答案(2)|浏览(149)

我想要达到的是:

type Post = {
  id: number
  title: string
  author: {
    name: string
  }
  comments: {
    text: string
  }[]
}

type ExtractPathExpressions<T> = ???

type Paths = ExtractPathExpressions<Post>
// expects above got a union --> 'id' | 'title' | 'author' | 'author.name' | 'comments' | `comments[${number}]` | `comments[${number}].text`

我知道这很不寻常...但是,有人知道ExtractPathExpressions会是什么样子吗?

iih3973s

iih3973s1#

这当然不是一个不寻常的任务,但它是一个复杂的递归任务,需要对不同的情况进行单独处理,其中属性:
1.是基本类型
1.是嵌套对象
1.是嵌套数组
第2种和第3种情况需要递归,因为这两种情况都可以包含其他嵌套对象和数组。
您希望创建所有可能的路径排列的 * 并集 *,因此在每一步,我们必须返回键本身和模板文字的并集,模板文字连接键和属性上递归ExtractPathExpressions的结果,除非它是基元类型。
类型本身显然应该是一个Map类型(在下面的示例中,我选择了较新的键重Map特性),其键可以用于模板文本类型(string | number | bigint | boolean | null | undefined的并集),这意味着必须排除symbol类型。
下面是所需类型的外观:

type ExtractPathExpressions<T, Sep extends string = "."> = Exclude<
  keyof {
    [P in Exclude<keyof T, symbol> as T[P] extends any[] | readonly any[]
      ?
          | P
          | `${P}[${number}]`
          | `${P}[${number}]${Sep}${Exclude<
              ExtractPathExpressions<T[P][number]>,
              keyof number | keyof string
            >}`
      : T[P] extends { [x: string]: any }
      ? `${P}${Sep}${ExtractPathExpressions<T[P]>}` | P
      : P]: string;
  },
  symbol
>;

测试:

type Post = {
  id: number
  title: string
  author: {
    name: string
  }
  comments: {
    text: string,
    replies: {
        author: {
            name: string
        }
    }[],
    responses: readonly { a:boolean }[],
    ids: string[],
    refs: number[],
    accepts: readonly bigint[]
  }[]
}

type Paths = ExtractPathExpressions<Post>;
//"id" | "title" | "author" | "comments" | "author.name" | `comments[${number}]` | `comments[${number}].text` | `comments[${number}].replies` | `comments[${number}].responses` | `comments[${number}].ids` | `comments[${number}].refs` | `comments[${number}].accepts` | `comments[${number}].replies[${number}]` | `comments[${number}].replies[${number}].author` | `comments[${number}].replies[${number}].author.name` | ... 4 more ... | `comments[${number}].accepts[${number}]`

Playground

smdncfj3

smdncfj32#

我也遇到过类似的问题,但我在提出的解决方案ExtractPathExpressions中发现了一个小问题。
如果你想得到一个数组的键,你将得到它的所有方法的键。

type ArrayPath = ExtractPathExpressions<[number, number]>;
// number | "length" | "toString" | "toLocaleString" | "pop" | "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" | "unshift" | "indexOf" | "lastIndexOf" | ... 19 more ... | "1"

我修改了这个解决方案来处理数组并提取子类型,以使代码更容易阅读。你可以在这里看到我的解决方案。

type ArrayPath = PathOf<[number, number]>; // "0" | "1"

完整代码:

// gets the path for any property in the type
export type PathOf<T> = Extract<keyof Flat<T>, string>;

// creates a flat type from interface or type
export type Flat<T, P extends string = '.'> = {
  [K in CustomKey<T> as T[K] extends any[] | readonly any[]
    ? FlatArrayKey<T[K], K, P>
    : T[K] extends AbstractObject
    ? FlatObjectKey<T[K], K, P>
    : K]: unknown;
};

// extract only those keys that have been defined by us
type CustomKey<T> = Exclude<
    keyof T,
    symbol | keyof Array<unknown> | keyof number | keyof string
>;

// helper
type AbstractObject = Record<string | number, any>;

// helper to create array key
type FlatArrayKey<A extends any[] | readonly any[], K extends string | number, P extends string> =
  | K
  | `${K}[${number}]`
  | `${K}[${number}]${P}${CustomKey<Flat<A[number]>>}`;

// helper to create object key
type FlatObjectKey<O extends AbstractObject, K extends string | number, P extends string> =
    | K
    | `${K}${P}${CustomKey<Flat<O>>}`;

示例:

type Post = {
  id: number
  title: string
  author: {
    name: string
  }
  comments: {
    text: string,
    replies: {
        author: {
            name: string
        }
    }[],
    responses: readonly { a:boolean }[],
    ids: string[],
    refs: number[],
    accepts: readonly bigint[]
  }[]
}

type Paths = PathOf<Post>;
//"id" | "title" | "author" | "comments" | "author.name" | `comments[${number}]` | `comments[${number}].text` | `comments[${number}].replies` | `comments[${number}].responses` | `comments[${number}].ids` | `comments[${number}].refs` | `comments[${number}].accepts` | `comments[${number}].replies[${number}]` | `comments[${number}].replies[${number}].author` | `comments[${number}].replies[${number}].author.name` | ... 4 more ... | `comments[${number}].accepts[${number}]

希望我帮到你了!

相关问题