递归更改 TypeScript 类型的属性名称,包括嵌套数组和可选属性 [英] Recursively changing property names of a TypeScript type, including nested arrays and optional properties

查看:26
本文介绍了递归更改 TypeScript 类型的属性名称,包括嵌套数组和可选属性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在 TypeScript 中,我正在研究一个通用的转换器"函数,该函数将获取一个对象并通过重命名其某些属性(包括嵌套数组和嵌套对象中的属性)来改变其形状.

In TypeScript, I'm working on a generic "transformer" function that will take an object and change its shape by renaming some of its properties, including properties in nested arrays and nested objects.

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

The actual renaming runtime code is easy, but I can't figure out the TypeScript typing. My type definition works for scalar properties and nested objects. But if a property is array-valued, the type definition loses type information for array elements. And if there are any optional properties on the object, type information is also lost.

我正在尝试做的事情可能吗?如果是,我如何支持数组属性和可选属性?

Is what I'm trying to do possible? If yes, how can I support array properties and optional properties?

我目前的解决方案是这个 StackOverflow 答案的组合(感谢 @jcalz!) 进行重命名和 这个 GitHub 示例(感谢 @ahejlsberg!)来处理递归部分.

My current solution is a combination of this StackOverflow answer (thanks @jcalz!) to do the renaming and this GitHub example (thanks @ahejlsberg!) to handle the recursive part.

下面的自包含代码示例(也在这里:https://codesandbox.io/s/kmyl013r3r) 显示什么有效,什么无效.

A self-contained code sample below (also here: https://codesandbox.io/s/kmyl013r3r) shows what's working and what's not.

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

推荐答案

有两个问题.

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

The one with arrays is due to the fact that you need to apply the TransformedValue logic to the E parameter not the Transform logic. That is you need to see if E is an array type (and change the element type only) or object type (and transform the property names) and if it's neither you need to leave it alone (it's probably a primitive and we should not map it). Right now since you apply Transform to E the result is primitives will get mangled by the rename process.

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

Since type aliases can't be recursive, we can define an interface derived from array, which will apply TransformedValue to its type parameter:

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

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

第二个问题与这样一个事实有关,如果接口具有可选属性并且接口通过同态映射类型,则成员的可选性将被保留,因此T[keyofT] 将包含 undefined.这会触发 KeyValueTupleToObject.最简单的解决方案是显式删除可选性

The second problem has to do with the fact that if an interface has optional properties and the interface is put through a homomorphic mapped type, the optionality of the members will be preserved, and thus the result of T[keyof T] will contain undefined. And this will trip-up KeyValueTupleToObject. The simplest solution is to remove the optionality explicitly

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

将它们放在一起应该可以工作:link

Putting it all together it should work: link

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

Edit A solution that keeps the types a bit more readable could use another of @jcalz answers that converts a union to an intersection (this one).

下面的解决方案也将保留类型的可选性,readonly 仍然丢失:

Also the solution below will keep the optionality of the types, readonly is still lost:

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;
//     }>;
// }

这篇关于递归更改 TypeScript 类型的属性名称,包括嵌套数组和可选属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆