What I want to achieve is this:
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`
I know this is unusual... but, does anyone know what would the ExtractPathExpressions
look like?
This is certainly not an unusual task, but it is a complex recursive one that requires separate handling for different cases where a property:
Recursion is required for cases 2 and 3 as both can contain other nested objects and arrays.
You want to create a union of all possible permutations of paths, so at each step, we have to return a union of the key itself and the template literal concatenating the key and a result of the recursive ExtractPathExpressions
on the property unless it is of primitive type.
The type itself should obviously be a mapped type (in the sample below I opted for the newer key remapping feature) with keys that can be used in template literal types (a union of string | number | bigint | boolean | null | undefined
), which means the symbol
type must be excluded.
This is how the desired type could look like:
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
>;
Testing it out:
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}]`