递归更改 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];
}
}
我认为这会有所帮助。
在 TypeScript 中,我正在研究一个通用的 "transformer" 函数,该函数将接受一个对象并通过重命名其某些属性(包括嵌套数组和嵌套对象中的属性)来更改其形状。
实际重命名运行时代码很简单,但我无法弄清楚 TypeScript 的类型。我的类型定义适用于标量属性和嵌套对象。但是如果 属性 是数组值的,则类型定义会丢失数组元素的类型信息。如果对象上有任何可选属性,类型信息也会丢失。
我想做的事情可行吗?如果是,如何支持数组属性和可选属性?
我目前的解决方案是
下面的独立代码示例(也在此处: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];
}
}
我认为这会有所帮助。