TypeScript 通用映射可变元组值到嵌套映射类型

TypeScript generic map variadic tuple values to nested mapped type

我正在尝试制作一个辅助函数,它接受像 JSON 这样的嵌套对象,并允许在任意深度深度复制嵌套值。我理解可变元组类型并且可以让它们工作以传递元组 - 但我不知道如何 'map' 它们到任意深度的嵌套 Picks (它甚至可能是不可能的)。这是我想出的最好的 - 但仍然限于需要为 GetNestedValue 创建尽可能多的重载,因为我愿意支持。我理解各种错误,我只是想不出任何方法来满足编译器并在 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');

link 至 playground

有趣的是,您偏爱“已知”类型而拒绝使用未知类型,到处都是一堆 anys。我对可变元组类型一无所知,但这里有 3 个示例

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;
}

我首先要说的是:虽然这是一个有趣的思想实验,但由于它需要大量的递归,我不推荐这样做。

它需要两种递归类型,一种类型用于获取从对象类型推断出的一组有效键,另一种类型用于访问 属性 给定那些经过验证的键。对于 TypeScript < 4.5,深度限制将是一个长度为 10 的元组。

验证:

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

为了让函数正确推断泛型,泛型需要作为可能的参数出现。我们想要的是 ValidKeys,但如果没有 Keys 本身作为潜在参数,我们就无法做到这一点。所以我们使用 ...keys 参数的条件来强制它解析。

关于 return 类型,即使 GetNestedProp 可能与 undefined 联合,编译器也无法推断它肯定是在您的 else 分支的情况下被击中。因此,您可以使 return 类型成为这种笨拙的条件,或者 //@ts-expect-error else 分支 return 语句具有更简单的 return 类型 GetNestedProp<Keys, Obj>。该替代方案包含在 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;
}

给定一个带有可选 属性 的类型,深入研究 属性 会将嵌套的 属性 类型转换为具有未定义的联合:

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

playground