代码之家  ›  专栏  ›  技术社区  ›  Justin Grant

递归更改TypeScript类型的属性名,包括嵌套数组和可选属性

  •  1
  • Justin Grant  · 技术社区  · 6 年前

    我想做的事可能吗?如果是,如何支持数组属性和可选属性?

    this StackOverflow answer (谢谢 @jcalz !) 进行重命名和 this GitHub example (谢谢 @ahejlsberg !) 处理递归部分。

    下面是一个独立的代码示例(也在这里: https://codesandbox.io/s/kmyl013r3r )显示哪些有效,哪些无效。

    // from https://stackoverflow.com/a/45375646/126352
    type ValueOf<T> = T[keyof T];
    type KeyValueTupleToObject<T extends [keyof any, any]> = {
      [K in T[0]]: Extract<T, [K, any]>[1]
    };
    type MapKeys<T, M extends Record<string, string>> = KeyValueTupleToObject<
      ValueOf<{ 
        [K in keyof T]: [K extends keyof M ? M[K] : K, T[K]] 
      }>
    >;
    
    // thanks to https://github.com/Microsoft/TypeScript/issues/22985#issuecomment-377313669
    export type Transform<T> = MapKeys<
      { [P in keyof T]: TransformedValue<T[P]> },
      KeyMapper
    >;
    type TransformedValue<T> = 
      T extends Array<infer E> ? Array<Transform<E>> :
      T extends object ? Transform<T> : 
      T;
    
    type KeyMapper = {
      foo: 'foofoo';
      bar: 'barbar';
    };
    
    // Success! Names are transformed. Emits this type:
    // type TransformOnlyScalars = {
    //   baz: KeyValueTupleToObject<
    //     ["foofoo", string] | 
    //     ["barbar", number]
    //   >;
    //   foofoo: string;
    //   barbar: number;
    // }
    export type TransformOnlyScalars = Transform<OnlyScalars>;
    interface OnlyScalars {
      foo: string;
      bar: number;
      baz: {
        foo: string;
        bar: number;
      }
    }
    export const fScalars = (a: TransformOnlyScalars) => {
      const shouldBeString = a.foofoo; // type is string as expected.
      const shouldAlsoBeString = a.baz.foofoo; // type is string as expected.
      type test<T> = T extends string ? true : never;
      const x: test<typeof shouldAlsoBeString>; // type of x is true
    };
    
    // Fails! Elements of array are not type string. Emits this type:
    // type TransformArray = {
    //    foofoo: KeyValueTupleToObject<
    //       string |
    //       number |
    //       (() => string) |
    //       ((pos: number) => string) |
    //       ((index: number) => number) |
    //       ((...strings: string[]) => string) |
    //       ((searchString: string, position?: number | undefined) => number) |
    //       ... 11 more ... |
    //       {
    //         ...;
    //       }
    //    > [];
    //    barbar: number;
    //  }
    export type TransformArray = Transform<TestArray>;
    interface TestArray {
      foo: string[];
      bar: number;
    }
    export const fArray = (a: TransformArray) => {
      const shouldBeString = a.foofoo[0];
      const s = shouldBeString.length; // type of s is any; no intellisense for string methods
      type test<T> = T extends string ? true : never;
      const x: test<typeof shouldBeString>; // type of x is never
    };
    
    // Fails! Property names are lost once there's an optional property. Emits this type:
    // type TestTransformedOptional = {
    //   [x: string]: 
    //     string | 
    //     number | 
    //     KeyValueTupleToObject<["foofoo", string] | ["barbar", number]> | 
    //     undefined;
    // }
    export type TransformOptional = Transform<TestOptional>;
    interface TestOptional {
      foo?: string;
      bar: number;
      baz: {
        foo: string;
        bar: number;
      }
    }
    export const fOptional = (a: TransformOptional) => {
      const shouldBeString = a.barbar; // type is string | number | KeyValueTupleToObject<["foofoo", string] | ["barbar", number]> | undefined
      const shouldAlsoBeString = a.baz.foofoo; // error: Property 'foofoo' does not exist on type 'string | number | KeyValueTupleToObject<["foofoo", string] | ["barbar", number]>'.
    };
    
    1 回复  |  直到 6 年前
        1
  •  3
  •   Titian Cernicova-Dragomir    6 年前

    有两个问题。

    TransformedValue 逻辑到 E 参数不是 Transform E 是数组类型(仅更改元素类型)或对象类型(并转换属性名),如果两者都不是,则不需要对其进行处理(它可能是一个基元,我们不应该映射它)。现在既然你申请了 变换 E 结果是原语将被重命名过程损坏。

    因为类型别名不能是递归的,所以我们可以定义一个从数组派生的接口,它将应用于 对于其类型参数:

    type TransformedValue<T> = 
        T extends Array<infer E> ? TransformedArray<E> :
        T extends object ? Transform<T> : 
        T;
    
    interface TransformedArray<T> extends Array<TransformedValue<T>>{}
    

    第二个问题与这样一个事实有关,即如果一个接口具有可选属性,并且该接口被放入同态映射类型,那么成员的可选性将被保留,从而导致 T[keyof T] undefined KeyValueTupleToObject . 最简单的解决方案是显式地去除可选性

    type MapKeys<T, M extends Record<string, string>> = KeyValueTupleToObject<
       ValueOf<{ 
           [K in keyof T]-?: [K extends keyof M ? M[K] : K, T[K]] 
       }>
    >;
    

    把这一切放在一起,它应该起作用: link

    编辑 一个使类型更具可读性的解决方案可以使用另一个@jcalz答案将并集转换为交集( this one ).

    同时,下面的解决方案将保持类型的可选性, readonly

    type UnionToIntersection<U> =
        (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
    
    type MapKeysHelper<T, K extends keyof T, M extends Record<string, string>> = K extends keyof M ? (
        Pick<T, K> extends Required<Pick<T, K>> ?
        { [P in M[K]]: T[K] } :
        { [P in M[K]]?: T[K] }
    ) : {
            [P in K]: T[P]
        }
    type Id<T> = { [P in keyof T]: T[P] }
    type MapKeys<T, M extends Record<string, string>> = Id<UnionToIntersection<MapKeysHelper<T, keyof T, M>>>;
    
    export type Transform<T> = MapKeys<
        { [P in keyof T]: TransformedValue<Exclude<T[P], undefined>> },
        KeyMapper
        >;
    type TransformedValue<T> =
        T extends Array<infer E> ? TransformedArray<E> :
        T extends object ? Transform<T> :
        T;
    
    interface TransformedArray<T> extends Array<TransformedValue<T>> { }
    
    type KeyMapper = {
        foo: 'foofoo';
        bar: 'barbar';
    };
    interface OnlyScalars {
        foo: string;
        bar: number;
        baz: {
            foo: string;
            bar: number;
        }
    }
    export type TransformOnlyScalars = Transform<OnlyScalars>;
    // If you hover you see:
    // {
    //     foofoo: string;
    //     barbar: number;
    //     baz: Id<{
    //         foofoo: string;
    //     } & {
    //         barbar: number;
    //     }>;
    // }
    
    
    interface TestArray {
        foo: string[];
        bar: number;
    }
    export type TransformArray = Transform<TestArray>;
    // If you hover you see:
    // {
    //     foofoo: TransformedArray<string>;
    //     barbar: number;
    // }
    
    interface TestOptional {
        foo?: string;
        bar: number;
        baz: {
            foo: string;
            bar: number;
        }
    }
    export type TransformOptional = Transform<TestOptional>;
    // If you hover you see:
    // {
    //     foofoo?: string | undefined;
    //     barbar: number;
    //     baz: Id<{
    //         foofoo: string;
    //     } & {
    //         barbar: number;
    //     }>;
    // }
    
        2
  •  0
  •   Darshan Bhavsar    5 年前

    *转换数组的函数调用*

    transformDataArrayOrObject() {
        // API Call Here
        console.log(this.some.reduceObjectOrArray([
          { Key1: '1', Key2: '2', Key3: '5' },
          { Key1: '2', Key2: '3', Key3: '6' },
          { Key1: '3', Key2: '4', Key3: '7' }
        ], ['Key1', 'Key3']));
      }
    

    一些服务 *

     // Map object (or array) having object with so many keys and reduce it to provided format i.e. newDefinition
      reduceObjectOrArray(data: any, newDefinition: any): any {
        const isDataArray = Array.isArray(data);
        data = isDataArray ? data : [data];
        const resData: any[] = [];
        data.forEach(item => {
          const obj: any = {};
          newDefinition.forEach(dataKey => {
            if (newDefinition.indexOf(dataKey) !== -1) {
              obj[dataKey] = item[dataKey];
            }
          });
          resData.push(obj);
        });
        return isDataArray ? resData : resData[0];
      }
    }
    

    我想这会有帮助的。