TipeScript generik memetakan nilai tuple variadik ke tipe yang dipetakan bersarang


Saya mencoba membuat fungsi pembantu yang mengambil objek bersarang seperti JSON, dan memungkinkan seseorang untuk membuat salinan mendalam dari nilai bersarang pada kedalaman mana pun. Saya memahami tipe tuple variadik dan dapat membuatnya bekerja hanya untuk melewatkan tuple - tetapi saya tidak tahu bagaimana 'memetakan' mereka ke Pick bersarang dengan kedalaman sembarang (mungkin bahkan tidak mungkin). Berikut adalah yang terbaik yang bisa saya buat - tetapi masih terbatas pada kebutuhan membuat sebanyak mungkin overload untuk GetNestedValue sesuai yang ingin didukung. Saya memahami berbagai kesalahannya, saya hanya tidak bisa menemukan cara untuk memuaskan compiler dan mendapatkan penyelesaian tipe pada nilai return.

// K is arbitrary length how to express N accessors deep? in TS without a loop?
type GetNestedValue<K extends string[], O extends any> = O[K[0]][K[1]][K[2]];

function getNestedItem<Keys extends string[], Obj>(
    obj: Obj, ...keys: readonly [...Keys]
): GetNestedValue<Keys, Obj> extends undefined ? undefined : GetNestedValue<Keys, Obj> {
    let level: any = obj;
    for (const key of keys) {
        if (level !== undefined) {
            level = level[key];
        } else {
            return;
        }
    }

    // this will return deepClone(level);
    return level;
}


const obj = {one: 1, two: {three: {four: 4}}};

// I'd prefer 'known' shapes of obj here block form entering invalid keys.
const a = getNestedItem(obj, 'one', 'two');

// here - when arbitrarily trying to grab stuff from unknown inputs - I don't want
// a warning, rather the user would just need to check `if (b !== undefined)`
const b = getNestedItem(obj as any, 'one', 'two');

tautan ke taman bermain

Jawaban #1

Saya akan memulai dengan mengatakan: Meskipun ini merupakan eksperimen pemikiran yang menyenangkan, saya tidak menyarankan ini karena jumlah rekursi yang dibutuhkannya.

Ini memerlukan dua tipe rekursif, sebuah tipe untuk mendapatkan set kunci yang valid yang diturunkan dari tipe objek, dan sebuah getter untuk mengakses properti yang diberikan kunci yang telah divalidasi tersebut. Untuk TypeScript < 4.5, batas kedalaman akan berupa tuple dengan panjang 10.

Validasi :

// walk through the keys and validate as we recurse. If we reach an invalid
// key, we return the currently validated set along with a type hint
type ValidatedKeys<K extends readonly PropertyKey[], O, ValidKeys extends readonly PropertyKey[] = []> = 
    K extends readonly [infer Key, ...infer Rest]
        // Excluding undefined to allow `a?.b?.c`
        ? Key extends keyof Exclude<O, undefined>
            ? Rest extends [] 
                ? [...ValidKeys, Key] // case: nothing left in the array, and the last item correctly extended `keyof O`.
                : Rest extends readonly PropertyKey[] // obligatory typeguard
                    ? ValidatedKeys<Rest,Exclude<O, undefined>[Key], [...ValidKeys, Key]> // recurse
                    : never // impossible, we've sufficiently typechecked `Rest`
            : [...ValidKeys, keyof Exclude<O, undefined>] // case: key doesn't exist on object at this level, adding `keyof O` will give a good type hint
        : [...ValidKeys] // case: empty top level array. This gives a good typehint for a single incorrect string;

Getter :

// access a property recursively. Utilizes the fact that `T | never` === `T`
type GetNestedProp<K extends readonly PropertyKey[], O, MaybeUndef extends undefined = never> = 
    K extends readonly [infer Key, ...infer Rest] 
        ? Key extends keyof O 
            ? Rest extends [] 
                ? O[Key] | MaybeUndef // succesful exit, no more keys remaining in array. Union with undefined if needed
                /* obligatory typeguard to validate the inferred `Rest` for recursion */
                : Rest extends readonly PropertyKey[]
                    // If it's potentially undefined, We're going to recurse excluding the undefined, and then unify it with an undefined
                    ? O[Key] extends infer Prop
                        ? Prop extends undefined
                            ? GetNestedProp<Rest, Exclude<Prop, undefined>, undefined>
                            : GetNestedProp<Rest,Prop, MaybeUndef>
                        : never // impossible, `infer Prop` has no constraint so will always succeed
                    :never // impossible, we've typechecked `Rest` sufficiently
            : undefined // case: key doesn't exist on object at this level
        : undefined; // case: empty top level array

Agar sebuah fungsi dapat menafsirkan sebuah generic dengan benar, generic tersebut perlu ada sebagai argumen yang mungkin. Apa yang kita inginkan adalah ValidKeys, tetapi kita tidak bisa melakukannya tanpa memiliki Keys itu sendiri sebagai argumen potensial. Jadi kita menggunakan kondisional untuk argumen ...keys agar memaksanya untuk diselesaikan.

Mengenai tipe pengembalian, meskipun GetNestedProp kemungkinan akan menjadi union dengan undefined, compiler tidak bisa menyimpulkan bahwa itu pasti terjadi pada kasus di mana cabang else Anda dijalankan. Jadi Anda bisa membuat tipe pengembalian berupa kondisi yang canggung ini, atau menggunakan //@ts-expect-error pada pernyataan return cabang else dengan tipe pengembalian yang lebih sederhana yaitu GetNestedProp<Keys, Obj>. Alternatif itu disertakan di playground:

function getNestedItem<Obj, Keys extends readonly [keyof Obj, ...PropertyKey[]], ValidKeys extends ValidatedKeys<Keys, Obj>>(
    obj: Obj,
    ...keys: ValidKeys extends Keys ? Keys : ValidKeys
): GetNestedProp<Keys, Obj> extends undefined ? GetNestedProp<Keys, Obj> | undefined : GetNestedProp<Keys,Obj> {
    let level: any = obj;
    for (const key of keys) {
        if (level !== undefined) {
            level = level[key];
        } else {
            return;
        }
    }
    return level;
}

Diberikan sebuah tipe dengan properti opsional, menggali ke dalam properti tersebut akan mengubah tipe properti bersarang menjadi gabungan dengan undefined:

interface HasOpt {
    a: { b: number };
    aOpt?: {b: number };
}
declare const obj: HasOpt;
const ab = getNestedItem(obj, "a", "b") // number
const abOpt = getNestedItem(obj, "aOpt", "b") // number | undefined

taman bermain

Jawaban #2

Lucu bagaimana kamu lebih suka tipe 'dikenal' dan menolak menggunakan yang tidak dikenal, memiliki banyak any di mana-mana. Saya tidak tahu apa-apa tentang tipe tuple variadik, tapi di sini kamu punya 3 contoh

export const getDeepProp = <Obj>(obj: Obj, ...keys: readonly [keyof Obj]) => {
    const send: Record<string, unknown> = {};


    keys.forEach(i=> {
        type current = typeof obj[typeof i];

        send[i] = obj[i] as current;
    });

    return send;
};



export function deepClone <O> (values:O): O {

      if (values == null || typeof values != "object") return values;

const copy: Record<string, unknown> = {};

        for (const attr in values) {
            if (values[attr]) {
                copy[attr] = deepClone(values[attr]);
            }
        }

        return copy as O;
}

export function name<O> (params:O, ...keys: string[]) {
    const a: Record<string,unknown> = {};

    keys.forEach(key => {
        a[key as keyof O] = typeof params[key as keyof O]==="object" ?
        name(params[key as keyof O], ...Object.keys(params[key as keyof O])) : params[key as keyof O];
    });

    return a as O;
}

Artikel terkait