За да постигнем тази цел, трябва да създадем пермутация на всички разрешени пътища. Например:
type Structure = {
user: {
name: string,
surname: string
}
}
type BlackMagic<T>= T
// user.name | user.surname
type Result=BlackMagic<Structure>
Проблемът става по-интересен с масиви и празни кортежи.
Кортежът, масивът с изрична дължина, трябва да се управлява по следния начин:
type Structure = {
user: {
arr: [1, 2],
}
}
type BlackMagic<T> = T
// "user.arr" | "user.arr.0" | "user.arr.1"
type Result = BlackMagic<Structure>
Логиката е пряка. Но как можем да се справим с number[]
? Няма гаранция, че индекс 1
съществува.
Реших да използвам user.arr.${number}
.
type Structure = {
user: {
arr: number[],
}
}
type BlackMagic<T> = T
// "user.arr" | `user.arr.${number}`
type Result = BlackMagic<Structure>
Все още имаме 1 проблем. Празен кортеж. Масив с нула елементи - []
. Трябва ли изобщо да разрешаваме индексиране? Не знам. Реших да използвам -1
.
type Structure = {
user: {
arr: [],
}
}
type BlackMagic<T> = T
// "user.arr" | "user.arr.-1"
type Result = BlackMagic<Structure>
Мисля, че най-важното тук е някаква конвенция. Можем също да използваме стрингови `"никога". Мисля, че от OP зависи как да се справи с него.
Тъй като знаем как трябва да се справим с различни случаи, можем да започнем внедряването. Преди да продължим, трябва да дефинираме няколко помощника.
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
}
Мисля, че именуването и тестовете са ясни. Поне искам да вярвам :D
Сега, когато имаме всички наши помощни програми, можем да дефинираме основната ни помощна програма:
/**
* 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>
Има малък проблем. Не трябва да връщаме props от най-високо ниво, като user
. Имаме нужда от пътища с поне една точка.
Има два начина:
- извлечете всички реквизити без точки
- предоставете допълнителен общ параметър за индексиране на нивото.
Две опции са лесни за изпълнение.
Получете всички подпори с dot (.)
:
type WithDot<T extends string> = T extends `${string}.${string}` ? T : never
Докато горният util е четим и поддържаем, вторият е малко по-труден. Трябва да предоставим допълнителен общ параметър и в двата Path
и HandleObject
.Вижте този пример, взет от друг въпрос
/ статия
:
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]
Честно казано, не мисля, че ще бъде лесно за някой да прочете това.
Трябва да приложим още нещо. Трябва да получим стойност чрез изчислен път.
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: [] }
}
Можете да намерите повече информация за използването на Reduce
в моя блог
.
Целият код:
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>
Това внедряването си струва да се обмисли