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

ice*_*itz 3 typescript mapped-types variadic-tuple-types

我正在尝试创建一个辅助函数,它采用 JSON 等嵌套对象,并允许在任意深度制作嵌套值的深层副本。我了解可变元组类型,并且可以让它们仅用于传递元组 - 但我不知道如何将它们“映射”到任意深度的嵌套选择(甚至可能不可能)。这是我想出的最好的方法 - 但仍然仅限于需要为 GetNestedValue 创建尽可能多的重载,因为我愿意支持。我理解各种错误,我只是想不出任何方法来满足编译器并在返回值上获得类型完成。

// 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');
Run Code Online (Sandbox Code Playgroud)

链接到游乐场

cdb*_*rin 5

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

它需要两种递归类型,一种是获取从对象类型推断出的有效键集的类型,另一种是 getter 来访问给定这些经过验证的键的属性。对于 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;
Run Code Online (Sandbox Code Playgroud)

吸气剂:

// 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
Run Code Online (Sandbox Code Playgroud)

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

关于返回类型,即使可能GetNestedProp是与 的联合undefined,编译器也无法推断出它肯定是在 else 分支被命中的情况下。因此,您可以将返回类型设置为这种尴尬的条件,或者//@ts-expect-error使用更简单的返回类型 的 else 分支返回语句GetNestedProp<Keys, Obj>。该替代方案包含在游乐场中:

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

操场