sql >> Base de Datos >  >> NoSQL >> MongoDB

Texto mecanografiado:clave profunda de un objeto anidado, con tipo relacionado

Para lograr este objetivo, necesitamos crear una permutación de todas las rutas permitidas. Por ejemplo:

type Structure = {
    user: {
        name: string,
        surname: string
    }
}

type BlackMagic<T>= T

// user.name | user.surname
type Result=BlackMagic<Structure>

El problema se vuelve más interesante con arreglos y tuplas vacías.

Tuple, la matriz con longitud explícita, debe administrarse de esta manera:

type Structure = {
    user: {
        arr: [1, 2],
    }
}

type BlackMagic<T> = T

// "user.arr" | "user.arr.0" | "user.arr.1"
type Result = BlackMagic<Structure>

La lógica es directa. Pero, ¿cómo podemos manejar number[] ? No hay garantía de que el índice 1 existe.

He decidido usar user.arr.${number} .

type Structure = {
    user: {
        arr: number[],
    }
}

type BlackMagic<T> = T

// "user.arr" | `user.arr.${number}`
type Result = BlackMagic<Structure>

Todavía tenemos 1 problema. Tupla vacía. Matriz con cero elementos - [] . ¿Necesitamos permitir la indexación? No sé. Decidí usar -1 .

type Structure = {
    user: {
        arr: [],
    }
}

type BlackMagic<T> = T

//  "user.arr" | "user.arr.-1"
type Result = BlackMagic<Structure>

Creo que lo más importante aquí es alguna convención. También podemos usar `"never" en cadena. Creo que depende de OP cómo manejarlo.

Como sabemos cómo debemos manejar los diferentes casos, podemos comenzar nuestra implementación. Antes de continuar, necesitamos definir varios ayudantes.

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

}

Creo que los nombres y las pruebas se explican por sí mismos. Al menos quiero creer :D

Ahora, cuando tenemos todo el conjunto de nuestras utilidades, podemos definir nuestra utilidad principal:

/**
 * 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>

Hay un pequeño problema. No deberíamos devolver accesorios de más alto nivel, como user . Necesitamos caminos con al menos un punto.

Hay dos formas:

  • extraer todos los accesorios sin puntos
  • proporciona un parámetro genérico adicional para indexar el nivel.

Dos opciones son fáciles de implementar.

Obtén todos los accesorios con dot (.) :

type WithDot<T extends string> = T extends `${string}.${string}` ? T : never

Si bien la utilidad anterior es legible y mantenible, la segunda es un poco más difícil. Necesitamos proporcionar un parámetro genérico adicional en ambos Path y HandleObject .Vea este ejemplo tomado de otro pregunta / artículo :

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]

Honestamente, no creo que sea fácil para nadie leer esto.

Tenemos que implementar una cosa más. Necesitamos obtener un valor por ruta calculada.


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: [] }
}

Puede encontrar más información sobre el uso de Reduce en mi blog .

Código completo:

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>

Playground

Esto vale la pena considerar la implementación