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

Recursively changing property names of a TypeScript type, including nested arrays and optional properties

在 TypeScript 中,我正在研究一个通用的 "transformer" 函数,该函数将接受一个对象并通过重命名其某些属性(包括嵌套数组和嵌套对象中的属性)来更改其形状。

实际重命名运行时代码很简单,但我无法弄清楚 TypeScript 的类型。我的类型定义适用于标量属性和嵌套对象。但是如果 属性 是数组值的,则类型定义会丢失数组元素的类型信息。如果对象上有任何可选属性,类型信息也会丢失。

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

我目前的解决方案是 (thanks @jcalz!) to do the renaming and this GitHub example (thanks @ahejlsberg!) 的组合来处理递归部分。

下面的独立代码示例(也在此处:https://codesandbox.io/s/kmyl013r3r)显示了哪些有效,哪些无效。

// from 
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]>'.
};

有两个问题。

带有数组的是因为您需要将 TransformedValue 逻辑应用于 E 参数而不是 Transform 逻辑。也就是说,您需要查看 E 是数组类型(并仅更改元素类型)还是对象类型(并转换 属性 名称),如果两者都不是,则无需单独处理(它是可能是原始的,我们不应该映射它)。现在,由于您将 Transform 应用于 E,结果是原语将被重命名过程破坏。

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

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

interface TransformedArray<T> extends Array<TransformedValue<T>>{}

第二个问题与这样一个事实有关,即如果接口具有可选属性并且接口通过同态映射类型放置,成员的可选性将被保留,因此 [=20= 的结果] 将包含 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 答案,将并集转换为交集 ()。

此外,下面的解决方案将保留类型的可选性,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;
//     }>;
// }

* 我的函数调用转换数组 *

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']));
  }

* some.service.ts *

中转换数组或对象的逻辑
 // 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];
  }
}

我认为这会有所帮助。