sql >> データベース >  >> NoSQL >> MongoDB

Typescript:ネストされたオブジェクトのディープキー、関連するタイプ

    この目標を達成するには、許可されているすべてのパスの順列を作成する必要があります。例:

    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>
    

    ここで最も重要なことは、いくつかの慣習だと思います。文字列化された`"never"を使用することもできます。どのように扱うかは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

    これで、utilsのセットがすべて揃ったら、メインのutilを定義できます:

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

    小さな問題があります。 userのような最高レベルの小道具を返さないでください 。少なくとも1つのドットが含まれるパスが必要です。

    2つの方法があります:

    • ドットなしですべての小道具を抽出します
    • レベルにインデックスを付けるための追加の汎用パラメータを提供します。

    2つのオプションは簡単に実装できます。

    dot (.)ですべての小道具を入手する :

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

    上記のutilは読み取り可能で保守可能ですが、2番目の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]
    

    正直なところ、誰もがこれを読むのは簡単ではないと思います。

    もう1つ実装する必要があります。計算されたパスで値を取得する必要があります。

    
    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の使用に関する詳細情報を見つけることができます <私のブログのrel="nofollow noreferrer noopener"href ="https://catchts.com/tuples#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>
    

    Playground

    これ 実装は検討する価値があります



    1. Key-Valueデータストアへのディレクトリ階層の保存

    2. (クライアント側の)javascriptを使用してRedisに直接接続しますか?

    3. null値のMongoDbクエリ配列

    4. 括弧と単語境界を持つMongo$regex