Để đạt được mục tiêu này, chúng ta cần tạo hoán vị của tất cả các đường dẫn được phép. Ví dụ:
type Structure = {
user: {
name: string,
surname: string
}
}
type BlackMagic<T>= T
// user.name | user.surname
type Result=BlackMagic<Structure>
Vấn đề trở nên thú vị hơn với các mảng và bộ giá trị trống.
Tuple, mảng có độ dài rõ ràng, nên được quản lý theo cách này:
type Structure = {
user: {
arr: [1, 2],
}
}
type BlackMagic<T> = T
// "user.arr" | "user.arr.0" | "user.arr.1"
type Result = BlackMagic<Structure>
Logic rất đơn giản. Nhưng làm thế nào chúng ta có thể xử lý number[]
? Không có gì đảm bảo rằng chỉ mục 1
tồn tại.
Tôi đã quyết định sử dụng user.arr.${number}
.
type Structure = {
user: {
arr: number[],
}
}
type BlackMagic<T> = T
// "user.arr" | `user.arr.${number}`
type Result = BlackMagic<Structure>
Chúng tôi vẫn còn 1 vấn đề. Bộ trống rỗng. Mảng không có phần tử - []
. Chúng ta có cần cho phép lập chỉ mục không? Tôi không biết. Tôi quyết định sử dụng -1
.
type Structure = {
user: {
arr: [],
}
}
type BlackMagic<T> = T
// "user.arr" | "user.arr.-1"
type Result = BlackMagic<Structure>
Tôi nghĩ điều quan trọng nhất ở đây là một số quy ước. Chúng ta cũng có thể sử dụng chuỗi ký tự `" không bao giờ ". Tôi nghĩ việc xử lý nó như thế nào là tùy thuộc vào OP.
Vì chúng tôi biết mình cần xử lý các trường hợp khác nhau như thế nào, chúng tôi có thể bắt đầu triển khai. Trước khi tiếp tục, chúng ta cần xác định một số trình trợ giúp.
type Values<T> = T[keyof T]
{
// 1 | "John"
type _ = Values<{ age: 1, name: 'John' }>
}
type IsNever<T> = [T] extends [never] ? true : false;
{
type _ = IsNever<never> // true
type __ = IsNever<true> // false
}
type IsTuple<T> =
(T extends Array<any> ?
(T['length'] extends number
? (number extends T['length']
? false
: true)
: true)
: false)
{
type _ = IsTuple<[1, 2]> // true
type __ = IsTuple<number[]> // false
type ___ = IsTuple<{ length: 2 }> // false
}
type IsEmptyTuple<T extends Array<any>> = T['length'] extends 0 ? true : false
{
type _ = IsEmptyTuple<[]> // true
type __ = IsEmptyTuple<[1]> // false
type ___ = IsEmptyTuple<number[]> // false
}
Tôi nghĩ việc đặt tên và các bài kiểm tra là tự giải thích. Ít nhất thì tôi muốn tin:D
Bây giờ, khi chúng ta có tất cả các utils của mình, chúng ta có thể xác định mục đích sử dụng chính của mình:
/**
* If Cache is empty return Prop without dot,
* to avoid ".user"
*/
type HandleDot<
Cache extends string,
Prop extends string | number
> =
Cache extends ''
? `${Prop}`
: `${Cache}.${Prop}`
/**
* Simple iteration through object properties
*/
type HandleObject<Obj, Cache extends string> = {
[Prop in keyof Obj]:
// concat previous Cacha and Prop
| HandleDot<Cache, Prop & string>
// with next Cache and Prop
| Path<Obj[Prop], HandleDot<Cache, Prop & string>>
}[keyof Obj]
type Path<Obj, Cache extends string = ''> =
// if Obj is primitive
(Obj extends PropertyKey
// return Cache
? Cache
// if Obj is Array (can be array, tuple, empty tuple)
: (Obj extends Array<unknown>
// and is tuple
? (IsTuple<Obj> extends true
// and tuple is empty
? (IsEmptyTuple<Obj> extends true
// call recursively Path with `-1` as an allowed index
? Path<PropertyKey, HandleDot<Cache, -1>>
// if tuple is not empty we can handle it as regular object
: HandleObject<Obj, Cache>)
// if Obj is regular array call Path with union of all elements
: Path<Obj[number], HandleDot<Cache, number>>)
// if Obj is neither Array nor Tuple nor Primitive - treat is as object
: HandleObject<Obj, Cache>)
)
// "user" | "user.arr" | `user.arr.${number}`
type Test = Extract<Path<Structure>, string>
Có một vấn đề nhỏ. Chúng tôi không nên trả lại các đạo cụ cấp cao nhất, như user
. Chúng tôi cần các đường dẫn có ít nhất một dấu chấm.
Có hai cách:
- trích xuất tất cả các đạo cụ không có dấu chấm
- cung cấp thêm thông số chung chung để lập chỉ mục cấp độ.
Hai tùy chọn rất dễ thực hiện.
Nhận tất cả đạo cụ bằng dot (.)
:
type WithDot<T extends string> = T extends `${string}.${string}` ? T : never
Mặc dù cách sử dụng trên có thể đọc được và có thể bảo trì, nhưng cách sử dụng thứ hai khó hơn một chút. Chúng tôi cần cung cấp thêm thông số chung trong cả Path
và HandleObject
.Xem ví dụ này được lấy từ câu hỏi
/ bài viết
:
type KeysUnion<T, Cache extends string = '', Level extends any[] = []> =
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`, [...Level, 1]>
: Level['length'] extends 1 // if it is a higher level - proceed
? KeysUnion<T[P], `${Cache}.${P}`, [...Level, 1]>
: Level['length'] extends 2 // stop on second level
? Cache | KeysUnion<T[P], `${Cache}`, [...Level, 1]>
: never
: never
}[keyof T]
Thành thật mà nói, tôi không nghĩ rằng sẽ dễ dàng cho bất kỳ ai đọc được điều này.
Chúng ta cần thực hiện một điều nữa. Chúng ta cần lấy một giá trị bằng đường dẫn được tính toán.
type Acc = Record<string, any>
type ReducerCallback<Accumulator extends Acc, El extends string> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends string,
Accumulator extends Acc = {}
> =
// Key destructure
Keys extends `${infer Prop}.${infer Rest}`
// call Reducer with callback, just like in JS
? Reducer<Rest, ReducerCallback<Accumulator, Prop>>
// this is the last part of path because no dot
: Keys extends `${infer Last}`
// call reducer with last part
? ReducerCallback<Accumulator, Last>
: never
{
type _ = Reducer<'user.arr', Structure> // []
type __ = Reducer<'user', Structure> // { arr: [] }
}
Bạn có thể tìm thêm thông tin về cách sử dụng Reduce
trong blog của tôi
.
Toàn bộ mã:
type Structure = {
user: {
tuple: [42],
emptyTuple: [],
array: { age: number }[]
}
}
type Values<T> = T[keyof T]
{
// 1 | "John"
type _ = Values<{ age: 1, name: 'John' }>
}
type IsNever<T> = [T] extends [never] ? true : false;
{
type _ = IsNever<never> // true
type __ = IsNever<true> // false
}
type IsTuple<T> =
(T extends Array<any> ?
(T['length'] extends number
? (number extends T['length']
? false
: true)
: true)
: false)
{
type _ = IsTuple<[1, 2]> // true
type __ = IsTuple<number[]> // false
type ___ = IsTuple<{ length: 2 }> // false
}
type IsEmptyTuple<T extends Array<any>> = T['length'] extends 0 ? true : false
{
type _ = IsEmptyTuple<[]> // true
type __ = IsEmptyTuple<[1]> // false
type ___ = IsEmptyTuple<number[]> // false
}
/**
* If Cache is empty return Prop without dot,
* to avoid ".user"
*/
type HandleDot<
Cache extends string,
Prop extends string | number
> =
Cache extends ''
? `${Prop}`
: `${Cache}.${Prop}`
/**
* Simple iteration through object properties
*/
type HandleObject<Obj, Cache extends string> = {
[Prop in keyof Obj]:
// concat previous Cacha and Prop
| HandleDot<Cache, Prop & string>
// with next Cache and Prop
| Path<Obj[Prop], HandleDot<Cache, Prop & string>>
}[keyof Obj]
type Path<Obj, Cache extends string = ''> =
(Obj extends PropertyKey
// return Cache
? Cache
// if Obj is Array (can be array, tuple, empty tuple)
: (Obj extends Array<unknown>
// and is tuple
? (IsTuple<Obj> extends true
// and tuple is empty
? (IsEmptyTuple<Obj> extends true
// call recursively Path with `-1` as an allowed index
? Path<PropertyKey, HandleDot<Cache, -1>>
// if tuple is not empty we can handle it as regular object
: HandleObject<Obj, Cache>)
// if Obj is regular array call Path with union of all elements
: Path<Obj[number], HandleDot<Cache, number>>)
// if Obj is neither Array nor Tuple nor Primitive - treat is as object
: HandleObject<Obj, Cache>)
)
type WithDot<T extends string> = T extends `${string}.${string}` ? T : never
// "user" | "user.arr" | `user.arr.${number}`
type Test = WithDot<Extract<Path<Structure>, string>>
type Acc = Record<string, any>
type ReducerCallback<Accumulator extends Acc, El extends string> =
El extends keyof Accumulator ? Accumulator[El] : El extends '-1' ? never : Accumulator
type Reducer<
Keys extends string,
Accumulator extends Acc = {}
> =
// Key destructure
Keys extends `${infer Prop}.${infer Rest}`
// call Reducer with callback, just like in JS
? Reducer<Rest, ReducerCallback<Accumulator, Prop>>
// this is the last part of path because no dot
: Keys extends `${infer Last}`
// call reducer with last part
? ReducerCallback<Accumulator, Last>
: never
{
type _ = Reducer<'user.arr', Structure> // []
type __ = Reducer<'user', Structure> // { arr: [] }
}
type BlackMagic<T> = T & {
[Prop in WithDot<Extract<Path<T>, string>>]: Reducer<Prop, T>
}
type Result = BlackMagic<Structure>
Điều này việc triển khai đáng được xem xét