TypeScript でディープ ピック関数を入力する


この記事では、lodash.get 関数に似たものを実装しようとします.

これは私が実装したいものです:

// John
const result = deepPick({ user: { name: 'John' } }, 'user', 'name') 


まず第一に、すべての引数を検証する必要がありますが、それらは正しい順序である必要があるためです.次のオブジェクトがあるとしましょう:

type Foo = {
    user: {
        description: {
            name: string;
            surname: string;
        }
    }
}


ここで、許可されたすべてのキーの配列を生成する必要があります.そのためには、すべてのキーを繰り返し処理し、type プロパティをキーの配列に置き換える必要があります.

それはあなたにとって理にかなっていますか?

type FirstAttempt<T> = {
    [P in keyof T]: [P]
}


上記のコードは意味がありません.ネストされたすべてのプロパティを反復処理する必要があるため、再帰的に作成する必要があります.もう一度試してみましょう.しかし今では、プロパティがプリミティブでない場合にのみ再帰を呼び出す必要があります.


type SecondAttempt<Obj> = {
    [Prop in keyof Obj]:
    Obj[Prop] extends PropertyKey
    ? [Prop]
    : SecondAttempt<Obj[Prop]>
}

// { name: ["name"]; surname: ["surname"]; }
type Result = SecondAttempt<Foo>['user']['description']



これで見栄えは良くなりましたが、名前と姓へのフル パスを受け取りませんでした.最後の非プリミティブ プロパティの配列しかありません.

キャッシュが必要なようです.

type ThirdAttempt<Obj, Cache extends Array<PropertyKey> = []> = {
    [Prop in keyof Obj]: Obj[Prop] extends PropertyKey
    ? [...Cache, Prop]
    : ThirdAttempt<Obj[Prop], [...Cache, Prop]>
}

// {
//     name: ["user", "description", "name"];
//     surname: ["user", "description", "surname"];
// }
type Result = ThirdAttempt<Foo>['user']['description']


今はずっと良く見えます.しかし、すべての可能な値が必要です.ユーザーは、プリミティブ値だけでなく取得できる必要があります.ユーザーが ['user', 'description'] を取得できるようにする必要があります
Cache を前の型と次の型の和集合として渡すことができます.

type FourthAttempt<Obj, Cache extends Array<PropertyKey> = []> = {
    [Prop in keyof Obj]: Obj[Prop] extends PropertyKey
    ? [...Cache, Prop]
    : FourthAttempt<Obj[Prop], Cache | [...Cache, Prop]>
}

type Result = FourthAttempt<Foo>['user']['description']


私たちは今、より近くにいるようです.奇妙なオブジェクトではなく、配列の結合が必要であることを覚えておいてください.ところで、それはまだ私たちの要件を満たしていません

条件ステートメントを 1 レベル上に移動しましょう.


type FifthAttempt<Obj, Cache extends Array<PropertyKey> = []> =
    Obj extends PropertyKey
    ? Cache
    : {
        [Prop in keyof Obj]:
        FifthAttempt<Obj[Prop], Cache | [...Cache, Prop]>
    }

type Result = FifthAttempt<Foo>


最も深いキーの値として、配列の結合が 1 か所にあるとします.どうすれば入手できますか?あなたが有名なユーティリティ タイプ type Values<T>=T[keyof T] を知っていることは間違いありません.

type Values<Obj> = Obj[keyof Obj]

type SixthAttempt0<Obj, Cache extends Array<PropertyKey> = []> =
    Obj extends PropertyKey
    ? Cache
    : Values<{
        [Prop in keyof Obj]:
        SixthAttempt0<Obj[Prop], Cache | [...Cache, Prop]>
    }>

type Result = SixthAttempt0<Foo>


コードにはまだ論理エラーがあります.直しましょう.

type FinalAttempt<Obj, Cache extends Array<PropertyKey> = []> =
    Obj extends PropertyKey
    ? Cache
    : {
        [Prop in keyof Obj]:
        | [...Cache, Prop]
        | FinalAttempt<Obj[Prop], [...Cache, Prop]>
    }[keyof Obj]

type Result = FinalAttempt<Foo>


ついにやった.つまらないか?気軽に休憩してください.

これまでのところ、作業の 50% しか完了していません.テストしてみましょう.

type FinalAttempt<Obj, Cache extends Array<PropertyKey> = []> =
    Obj extends PropertyKey ? Cache : {
        [Prop in keyof Obj]:
        | [...Cache, Prop]
        | FinalAttempt<Obj[Prop], [...Cache, Prop]>
    }[keyof Obj]

declare function deepPick<Obj,>(obj: Obj, ...keys: FinalAttempt<Obj>): void
declare var foo: Foo;

deepPick(foo, 'user'); // ok
deepPick(foo, 'user', 'description') // ok
deepPick(foo, 'description') // expected error


私たちの ReturnType はどうですか?

ところで、この関数の実装を記述しようとすると、エラーが発生します.

function deepPick<Obj >(obj: Obj, ...keys: FinalAttempt<Obj>){
    return keys.reduce((acc,elem)=>acc[elem], obj) // <-- errors
}


これを機能させるには、keys に無限再帰の問題がなく、文字列の配列であることを TS に保証する必要があります.さらに、リデューサー述語をどのように入力しますか?したがって、反復ごとに異なる型が返されます.

キーの結合 ( FinalAttempt ) を入力したのと同じ方法で入力できますが、今回は値の結合を作成しましょう. Foo 個の値のすべての組み合わせを返します.

type ValuesUnion<Obj, Cache = Obj> =
    Obj extends Primitives ? Obj : Values<{
        [Prop in keyof Obj]:
        | Cache | Obj[Prop]
        | ValuesUnion<Obj[Prop], Cache | Obj[Prop]>
    }>


ここでも単純な hasProperty タイプガードを使用しました.

const hasProperty = <Obj, Prop extends Primitives>(obj: Obj, prop: Prop)
    : obj is Obj & Record<Prop, any> =>
    Object.prototype.hasOwnProperty.call(obj, prop);

function deepPick<Obj, Keys extends FinalAttempt<Obj> & Array<string>>
    (obj: ValuesUnion<Obj>, ...keys: Keys) {
    return keys
        .reduce(
            (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc,
            obj
        )
}


オブジェクトのプロパティを選択するための型を書きましょう.関数で行ったのとまったく同じ方法でディープ ピッキングを実装することにしました.ボンネットの下でどのように機能するかを理解するのに役立つと思います.わかりました、わかりました、あなたは私を咳しました.他の方法でそれを行う方法がわかりません:)

type Elem = string;

type Acc = Record<string, any>

// (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc
type Predicate<Accumulator extends Acc, El extends Elem> =
    El extends keyof Accumulator ? Accumulator[El] : Accumulator

type Reducer<
    Keys extends ReadonlyArray<Elem>,
    Accumulator extends Acc = {}
    > =
    /**
     *  If Keys is empty array, no need to call recursion, 
     *  just return Accumulator
     */
    Keys extends []
    ? Accumulator
    /**
     * If keys is one element array,
     * 
     */
    : Keys extends [infer H]
    ? H extends Elem
    /**
     * take this element and call predicate 
     */
    ? Predicate<Accumulator, H>
    : never
    /**
     * If Keys is an Array of more than one element
     */
    : Keys extends readonly [infer H, ...infer Tail]
    ? Tail extends ReadonlyArray<Elem>
    ? H extends Elem
    /**
     * Call recursion with Keys Tail
     * and call predicate with first element 
     */
    ? Reducer<Tail, Predicate<Accumulator, H>>
    : never
    : never
    : never;


私がここで何をしたかを理解するのが難しい場合でも、心配はいりません.私にとっても難しいことです.

ここでは、純粋な js アナロジーがあります.

const reducer = (keys: string[], accumulator: Record<string, any> = {}) => {
    const predicate = (obj,prop)=>obj[prop]

    if (keys.length === 0) {
        return accumulator;
    }
    if (keys.length === 1) {
        const [head] = keys;
        return reducer([], predicate(accumulator, head))
    }

    if(keys.length>1){
        const [head, ...tail]=keys;
        return reducer(tail, predicate(accumulator, head))
    }
}


わかりました、完了です.完全なコードは次のとおりです.

type Foo = {
    user: {
        description: {
            name: string;
            surname: string;
        }
    }
}

declare var foo: Foo;

/**
 * Common utils
 */

type Primitives = string | number | symbol;

type Values<T> = T[keyof T]

type Elem = string;

type Acc = Record<string, any>

// (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc
type Predicate<Accumulator extends Acc, El extends Elem> =
    El extends keyof Accumulator ? Accumulator[El] : Accumulator

type Reducer<
    Keys extends ReadonlyArray<Elem>,
    Accumulator extends Acc = {}
    > =
    /**
     *  If Keys is empty array, no need to call recursion, 
     *  just return Accumulator
     */
    Keys extends []
    ? Accumulator
    /**
     * If keys is one element array,
     * 
     */
    : Keys extends [infer H]
    ? H extends Elem
    /**
     * take this element and call predicate 
     */
    ? Predicate<Accumulator, H>
    : never
    /**
     * If Keys is an Array of more than one element
     */
    : Keys extends readonly [infer H, ...infer Tail]
    ? Tail extends ReadonlyArray<Elem>
    ? H extends Elem
    /**
     * Call recursion with Keys Tail
     * and call predicate with first element 
     */
    ? Reducer<Tail, Predicate<Accumulator, H>>
    : never
    : never
    : never;

const hasProperty = <Obj, Prop extends Primitives>(obj: Obj, prop: Prop)
    : obj is Obj & Record<Prop, any> =>
    Object.prototype.hasOwnProperty.call(obj, prop);


/**
 * Fisrt approach
 * 
 */

type KeysUnion<T, Cache extends Array<Primitives> = []> =
    T extends Primitives ? Cache : {
        [P in keyof T]:
        | [...Cache, P]
        | KeysUnion<T[P], [...Cache, P]>
    }[keyof T]

type ValuesUnion<T, Cache = T> =
    T extends Primitives ? T : Values<{
        [P in keyof T]:
        | Cache | T[P]
        | ValuesUnion<T[P], Cache | T[P]>
    }>

function deepPickFinal<Obj, Keys extends KeysUnion<Obj> & ReadonlyArray<string>>
    (obj: ValuesUnion<Obj>, ...keys: Keys): Reducer<Keys, Obj>

function deepPickFinal<Obj, Keys extends KeysUnion<Obj> & Array<string>>
    (obj: ValuesUnion<Obj>, ...keys: Keys) {
    return keys
        .reduce(
            (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc,
            obj
        )
}

/**
 * Ok
 */
const result = deepPickFinal(foo, 'user') // ok
const result2 = deepPickFinal(foo, 'user', 'description') // ok
const result3 = deepPickFinal(foo, 'user', 'description', 'name') // ok
const result4 = deepPickFinal(foo, 'user', 'description', 'surname') // ok

/**
 * Expected errors
 */
const result5 = deepPickFinal(foo, 'surname')
const result6 = deepPickFinal(foo, 'description')
const result7 = deepPickFinal(foo)


それを行う別のアプローチがあります. validation technique を使用する必要があります.

私は知っています、ネーミングはひどいものです:)もっと意味のある名前を付ける方法がわかりません.

type Foo = {
    user: {
        description: {
            name: string;
            surname: string;
        }
    }
}

declare var foo: Foo;

type Primitives = string | number | symbol;


type Util<Obj, Props extends ReadonlyArray<Primitives>> =
    Props extends []
    ? Obj
    : Props extends [infer First]
    ? First extends keyof Obj
    ? Obj[First]
    : never
    : Props extends [infer Fst, ...infer Tail]
    ? Fst extends keyof Obj
    ? Tail extends string[]
    ? Util<Obj[Fst], Tail>
    : never
    : never
    : never

// credits https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379091887
type IsNeverType<T> = [T] extends [never] ? true : false;

type IsAllowed<T> = IsNeverType<T> extends true ? false : true;
type Validator<T extends boolean | string> = T extends true ? [] : [never]
type ValuesUnion<T, Cache = T> =
    T extends Primitives ? T : {
        [P in keyof T]:
        | Cache | T[P]
        | ValuesUnion<T[P], Cache | T[P]>
    }[keyof T]

const hasProperty = <Obj, Prop extends Primitives>(obj: Obj, prop: Prop)
    : obj is Obj & Record<Prop, any> =>
    Object.prototype.hasOwnProperty.call(obj, prop);


function pick<
    Obj,
    Prop extends string,
    Props extends ReadonlyArray<Prop>,
    Result extends Util<Obj, Props>>
    (
        obj: ValuesUnion<Obj>,
        props: [...Props],
        ..._: Validator<IsAllowed<Result>>
    ): Util<Obj, Props>;

function pick<
    Obj,
    Prop extends string,
    Props extends ReadonlyArray<Prop>,
    Result extends Util<Obj, Props>>(
        obj: ValuesUnion<Obj>,
        props: [...Props],
        ..._: Validator<IsAllowed<Result>>) {
    return props.reduce(
        (acc, prop) => hasProperty(acc, prop) ? acc[prop] : acc,
        obj
    )
}

/**
 * Ok
 */
const result8 = pick(foo, ['user', 'description']) // ok
const result9 = pick(foo, ['user', 'description', 'name']) // ok

/**
 * Expected errors
 */

const result10 = pick(foo, ['description']) // error
const result11 = pick(foo, ['name']) // ok


それで全部です.ありがとうございます