Рекурсивно изменяющиеся имена свойств типа TypeScript, включая вложенные массивы и необязательные свойства - PullRequest
0 голосов
/ 02 ноября 2018

В TypeScript я работаю над общей функцией-трансформером, которая будет принимать объект и изменять его форму, переименовывая некоторые из его свойств, включая свойства во вложенных массивах и вложенных объектах.

Фактическое переименование кода времени выполнения легко, но я не могу понять, как печатать на TypeScript. Мое определение типа работает для скалярных свойств и вложенных объектов. Но если свойство имеет массив значений, определение типа теряет информацию о типе для элементов массива. И если есть какие-либо дополнительные свойства объекта, информация о типе также теряется.

Возможно ли то, что я пытаюсь сделать? Если да, как я могу поддерживать свойства массива и дополнительные свойства?

Мое текущее решение - это комбинация этого ответа StackOverflow (спасибо @ jcalz !) Для переименования и этого примера GitHub (спасибо @ 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 Ответ

0 голосов
/ 02 ноября 2018

Есть две проблемы.

Тот, у которого есть массивы, связан с тем, что вам нужно применить логику 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;
//     }>;
// }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...