Om dit doel te bereiken, moeten we een permutatie van alle toegestane paden creëren. Bijvoorbeeld:
type Structure = {
user: {
name: string,
surname: string
}
}
type BlackMagic<T>= T
// user.name | user.surname
type Result=BlackMagic<Structure>
Het probleem wordt interessanter met arrays en lege tuples.
Tuple, de array met expliciete lengte, moet op deze manier worden beheerd:
type Structure = {
user: {
arr: [1, 2],
}
}
type BlackMagic<T> = T
// "user.arr" | "user.arr.0" | "user.arr.1"
type Result = BlackMagic<Structure>
Logica is rechttoe rechtaan. Maar hoe kunnen we omgaan met number[]
? Er is geen garantie dat index 1
bestaat.
Ik heb besloten om user.arr.${number}
. te gebruiken .
type Structure = {
user: {
arr: number[],
}
}
type BlackMagic<T> = T
// "user.arr" | `user.arr.${number}`
type Result = BlackMagic<Structure>
We hebben nog 1 probleem. Lege tupel. Array met nul elementen - []
. Moeten we indexeren überhaupt toestaan? Ik weet het niet. Ik besloot om -1
. te gebruiken .
type Structure = {
user: {
arr: [],
}
}
type BlackMagic<T> = T
// "user.arr" | "user.arr.-1"
type Result = BlackMagic<Structure>
Ik denk dat het belangrijkste hier een conventie is. We kunnen ook stringified `"nooit" gebruiken. Ik denk dat het aan OP is hoe ermee om te gaan.
Omdat we weten hoe we verschillende gevallen moeten behandelen, kunnen we beginnen met de implementatie. Voordat we verder gaan, moeten we verschillende helpers definiëren.
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
}
Ik denk dat naamgeving en tests voor zich spreken. Ik wil het tenminste geloven :D
Nu, als we al onze hulpprogramma's hebben, kunnen we ons belangrijkste hulpprogramma definiëren:
/**
* 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>
Er is een klein probleem. We mogen geen rekwisieten van het hoogste niveau retourneren, zoals user
. We hebben paden nodig met ten minste één punt.
Er zijn twee manieren:
- haal alle rekwisieten uit zonder stippen
- geef een extra generieke parameter voor het indexeren van het niveau.
Twee opties zijn eenvoudig te implementeren.
Verkrijg alle rekwisieten met dot (.)
:
type WithDot<T extends string> = T extends `${string}.${string}` ? T : never
Hoewel bovenstaande util leesbaar en onderhoudbaar is, is de tweede een beetje moeilijker. We moeten een extra generieke parameter opgeven in beide Path
en HandleObject
.Zie dit voorbeeld van andere vraag
/ artikel
:
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]
Eerlijk gezegd denk ik niet dat het voor iemand gemakkelijk zal zijn om dit te lezen.
We moeten nog één ding implementeren. We moeten een waarde verkrijgen per berekend pad.
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: [] }
}
U kunt meer informatie vinden over het gebruik van Reduce
in mijn blog
.
Hele code:
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>
Dit implementatie is het overwegen waard