Есть две проблемы.
Тот, у которого есть массивы, связан с тем, что вам нужно применить логику 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>>{}
Вторая проблема связана с тем фактом, что если интерфейс имеет необязательные свойства, и интерфейс передается через гомоморфный сопоставленный тип, необязательность членов будет сохранена, и, следовательно, результат 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]]
}>
>;
Собрав все вместе, оно должно работать: ссылка
Редактировать
Решение, которое делает типы более читабельными, может использовать другой из ответов @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;
// }>;
// }