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

标签 typescript mapped-types variadic-tuple-types

我正在尝试创建一个辅助函数,它接受像 JSON 这样的嵌套对象,并允许在任意深度深度复制嵌套值。我了解可变元组类型,并且可以让它们为传递元组而工作——但我不知道如何将它们“映射”到任意深度的嵌套 Picks(甚至可能不可能)。这是我想出的最好的 - 但仍然限于需要为 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');

链接到 playground

最佳答案

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

它需要两种递归类型,一种是获取从对象类型推断出的一组有效键的类型,另一种是用于访问给定这些经过验证的键的属性的 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;

setter/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 使用条件强制它解决的论点。

关于返回类型,即使GetNestedProp可能与 undefined 联合,编译器无法推断它肯定是在您的 else 分支被命中的情况下。因此,您可以将返回类型设为这种笨拙的条件,或者 //@ts-expect-error else 分支返回语句,返回类型更简单 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

关于TypeScript 通用映射可变元组值到嵌套映射类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68867854/

相关文章:

typescript - Vuejs Typescript 类组件 refs.focus 不工作

typescript - 给定判别式,获取判别联合的相应成员的属性类型

用于除其他数组之外的任何数组的 typescript 接口(interface)

Typescript - 添加额外的函数参数

angular - 类型错误 : Cannot read property 'flags' of undefined 中的错误

typescript :声明与另一个类型相同的变量

node.js - 使用 typescript 2 使用 sequelize 模型扩展 express 请求对象

typescript - 如何在 Typescript 中将接口(interface)转换为映射类型

typescript - 我可以在不通过断言强制类型的情况下修复此 typescript 编译器错误吗?

c++ - 如何获取元组 C++ 的前 N ​​个元素?