【问题标题】:Typescript: save type when mutating accumulator inside Array.reduce打字稿:在 Array.reduce 中改变累加器时保存类型
【发布时间】:2021-07-04 23:51:38
【问题描述】:

此函数将深度嵌套的属性添加到对象,将格式为 'a.very.ddep.property' 的字符串作为参数。

function nest<B extends obj, V = unknown>(
    target: B,
    structure: string,
    value: V,
) {
    const properties = structure.split('.');
    const result = target;
    properties.reduce((acc, property, i, arr) => {
        const isLastProperty = i === arr.length - 1;
        if (!(property in acc))
            acc[property] = isLastProperty ? value : {};
        return acc[property];
    }, target);
    return target;
}

它在 Javascript 中运行良好,但在 Typescript 中我收到错误 Type 'string' cannot be used to index type 'B' 试图分配 accum[property]。 通常我可以通过创建另一个具有交集类型的对象来避免改变 acc,但使用 reduce 表明我必须改变 acc insude 回调以获得最终结果。

(accum as B &amp; { [property: string]: obj | V })[property] = isLastProperty ? value : {}; 也不起作用,给我错误type 'string' cannot be used to index type 'B &amp; { [property: string]: obj | V; } 那有什么办法呢?

【问题讨论】:

    标签: typescript object mutation


    【解决方案1】:

    由于 TypeScript 是静态类型的,因此您尝试做的事情与 TypeScript 的配合并不好,但生成的拆分 properties 数组将是动态的,并且可能只能在运行时确定。

    还是可以做的,就是有点啰嗦:

    function nest<B extends object, V = unknown>(
        target: B,
        structure: string,
        value: V,
    ) {
        const properties = structure.split('.');
        const result = target;
        const lastProp = properties.pop();
        const lastObj = properties.reduce((acc, property) => {
            if (!(property in acc))
                acc[property] = {};
            const nestedVal = acc[property];
            if (!nestedVal || typeof nestedVal !== 'object') {
                throw new Error();
            }
            return nestedVal;
        }, target);
        lastObj[lastProp] = value;
        return target;
    }
    

    重要部分包括:

    • reduce 的末尾添加最终的嵌套值,以便reduce 回调可以一致地正确键入为&lt;object&gt;
    • 如果属性不存在或不是对象,则在回调中引发错误以保证类型安全
    • 首先将acc[property] 提取到一个独立变量中,以便对其进行缩小(不能像in 那样缩小acc[property]

    【讨论】:

    • 感谢您的回答! Нeah,Typescript 需要稍微不同的思维方式...您的代码仍然会在启用严格检查的情况下出现错误 ("Element implicitly has an 'any' type...' acc[property]")。这可以通过将 &lt;B extends object&gt; 更改为 &lt;B extends Record&lt;string, any&gt; 来解决,然后它变成"Type 'string' cannot be used to index type 'B'"。此外,生成的类型不包括添加的属性。
    【解决方案2】:

    这并不容易,但使用 Typescript 4 模板文字,我设法制作了一个类型安全的版本。

        type DeepType<T, S extends string> = T extends object
                ? S extends `${infer Key}.${infer NextKey}`
                    ? Key extends keyof T
                        ? DeepType<T[Key], NextKey>
                        : false
                    : S extends keyof T
                    ? T[S]
                    : never
                : T;
            
        type RecursiveKeyOf<TObj extends object> = {
                [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue<
                    TObj[TKey],
                    `${TKey}`
                >;
            }[keyof TObj & (string | number)];
            type RecursiveKeyOfInner<TObj extends object> = {
                [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue<
                    TObj[TKey],
                    RecursiveKeyOfAccess<TKey>
                >;
            }[keyof TObj & (string | number)];
            type RecursiveKeyOfHandleValue<
                TValue,
                Text extends string
            > = TValue extends object
                ? Text | `${Text}${RecursiveKeyOfInner<TValue>}`
                : Text;
            type RecursiveKeyOfAccess<TKey extends string | number> =
                | `['${TKey}']`
                | `.${TKey}`;
    type obj = Record<string, any>;
    
    export function addNestedProperty<
        B extends Record<string, any>,
        P extends string & RecursiveKeyOf<B>,
        V extends DeepType<B, P>
    >(
        base: B,
        path: P,
        value: V,
        o: { overwrite: boolean } = { overwrite: true },
    ): B {
        const properties = path.split('.') as Array<string & keyof B>;
        const lastProperty = properties.pop();
        if (!lastProperty)
            throw new Error('path argument must contain at least one property');
    
        function isObject(arg: unknown): arg is object {
            return typeof arg === 'object' && arg !== null;
        }
    
        const lastObj = properties.reduce((acc, property) => {
            if (!(property in acc)) acc[property] = {} as any;
            else if (!isObject(acc) && !o.overwrite)
                throw new Error('you are trying to overwrite existing property');
            return acc[property];
        }, base);
    
        lastObj[lastProperty] = value;
        return base;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-08
      • 2020-04-06
      • 2021-03-25
      • 2017-05-18
      • 2022-12-07
      • 1970-01-01
      • 2018-06-08
      • 2022-08-09
      相关资源
      最近更新 更多