Типизация для выравнивания объекта на основе его свойств идентификатора и метки

Детская площадка

Я хочу сгладить объект следующей формы

{
  article: 'prova',
  id: 63,
  topology: { id: 'topId', label: 'topLabel' },
  something: { id: 'someId', label: 'someLabel' }
}

во что-то со следующим

{
  article: "prova",
  id: 63,
  topId: "topLabel",
  someId: "someLabel"
}

Типы Input и Output должны удовлетворять строгой типизации. По сути, должно быть справедливо следующее:

interface Input {
  article: string
  id: number
  abc: { id: string; label: string }
  def: { id: string; label: string }
}
interface Output {
  article: string
  id: number
  topId: string
  someId: string
}
const input1: Input = {
  article: 'prova',
  id: 63,
  abc: { id: 'topId', label: 'topLabel' },
  def: { id: 'someId', label: 'someLabel' }
}
const input2 = {
  article: 'prova',
  id: 63,
  abc: { id: 'topId', label: 'topLabel' },
  def: { id: 'someId', label: 'someLabel' }
}
// Type 'Flattened<string | number | null | undefined, { id: string; label: string; }, Record<string, string | number | { id: string; label: string; } | null | undefined>>' is missing the following properties from type 'Output': article, id, topId, someId
// Argument of type 'Input' is not assignable to parameter of type 'Record<string, string | number | { id: string; label: string; } | null | undefined>'.
// Index signature for type 'string' is missing in type 'Input'.
const output1: Output = flattenObject(input1)
// Type 'Flattened<string | number | null | undefined, { id: string; label: string; }, { article: string; id: number; abc: { id: string; label: string; }; def: { id: string; label: string; }; }>' is missing the following properties from type 'Output': topId, someId
const output2: Output = flattenObject(input2)

Я попробовал эту предварительную реализацию, но TypeScript недоволен:

function hasId<
  K extends string | number | undefined | null,
  T extends { id: string; label: string }
>(el: [string, K | T]): el is [string, T] {
  return (el[1] as T).id != null
}

type Flattened<
  K extends string | number | undefined | null,
  T extends { id: string; label: string },
  S extends Record<string, K | T>
> = {
  [key in keyof S as S[key] extends T ? S[key]['id'] : key]: S[key] extends T
    ? S[key]['label']
    : S[key]
}

const flattenObject = <
  K extends string | number | undefined | null,
  T extends { id: string; label: string },
  S extends Record<string, K | T>
>(
  obj: S
): Flattened<K, T, S> =>
  Object.fromEntries(
    Object.entries(obj).map<[string, K | string]>((el) =>
      hasId(el) ? [el[1].id, el[1].label] : (el as [string, K])
    )
  ) as Flattened<K, T, S>

• Обратите внимание, что фразы «TypeScript недоволен» недостаточно, чтобы это был минимально воспроизводимый пример. Пожалуйста, добавьте комментарии в коде, где выражается недовольство, и точные формулировки любых сообщений, выражающих такое недовольство. • В этом коде много дополнительных/неиспользуемых параметров типа, что делает проблему менее очевидной. Вам нужно, чтобы мы отладили ваш код или просто создали новый работающий код?

jcalz 19.07.2024 14:21

(см. предыдущий комментарий) Если вам просто нужна помощь в создании работающего кода, соответствует ли эта версия вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?

jcalz 19.07.2024 14:34

Привет и спасибо за ваш комментарий. Я не добавлял ошибки TS, потому что в одной строке их было несколько, и я уже опубликовал игровую площадку, где вам будет легче их увидеть, но сейчас я добавил их в вопрос. Прошу прощения за дополнительные параметры типа: это была часть более сложной функции, и я забыл их удалить. Новый работающий код был бы достаточно хорош, но, к сожалению, на вашей игровой площадке ваш ввод является константой, и у меня нет такой роскоши, потому что тип уже назначен. Есть ли способ обойти это?

darkbasic 19.07.2024 18:25

Нет, нет пути вокруг этого. если вы комментируете как Input, то TS понятия не имеет, что такое реквизит id, кроме string. Поэтому ответ здесь либо «используйте литеральные типы», либо «это невозможно». Какой из них вы хотели бы увидеть написанным?

jcalz 19.07.2024 19:20

Ваша версия с литеральными типами все еще может быть полезна в некоторых случаях, поэтому я бы сохранил ее и отметил, что более обобщенный подход невозможен. Спасибо.

darkbasic 19.07.2024 20:57
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
3
5
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

ВАЖНОЕ ПРЕДУПРЕЖДЕНИЕ: если вам требуется, чтобы ввод был аннотирован как тип Input, как в

interface Input {
  article: string
  id: number
  abc: { id: string; label: string }
  def: { id: string; label: string }
}
const input: Input = { ⋯ };

тогда это совершенно невозможно

const output: Output = flattenObject(input);

работать напрямую. TypeScript знает только, что input выше имеет тип Input, поэтому тип input.abc.id — это просто string, как и тип input.def.id. Вся информация о значениях "topId" и "someId" утеряна. Лучшее, что вы получите, — это flattenObject(input) создать значение типа {article: string; id: number} & {[x: string]: string}, чего недостаточно.

Если вы хотите, чтобы это работало, вы должны сообщить TypeScript, что input.abc.id имеет литеральный тип"topId" и что input.def.id имеет литеральный тип "someId". Вы можете сделать это, изменив Input на

interface Input {
  article: string
  id: number
  abc: { id: "topId"; label: string }
  def: { id: "someId"; label: string }
}

или разрешив input иметь более узкий тип, чем Input, например

const input = { ⋯ } as const satisfies Input;

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

Что касается остальной части этого ответа, я предполагаю, что вы можете это сделать.


Я бы написал Flattened<T> и flattenObject лайк

type Flattened<T extends object> =
  { [K in keyof T as T[K] extends IdLabel ? T[K]["id"] : K]:
    T[K] extends IdLabel ? T[K]["label"] : T[K]
  }

interface IdLabel { id: string; label: string }

declare const flattenObject: <T extends object>(
  obj: T
) => Flattened<T>

По сути, это сопоставленный тип 🔁 ключа с переназначением свойство T у каждой клавиши K проверяется, чтобы определить, является ли это IdLabel или нет. Если это IdLabel, то ключ переназначается на свойство id, а значение сопоставляется со свойством label. В противном случае ключ и значение остаются в покое.

Фактическая реализация flattenObject здесь, вероятно, не подлежит сомнению, но для полноты картины вы могли бы написать

function isIdLabel(x: any): x is IdLabel {
  return !!x && (typeof x === "object") && ("id" in x) && 
    (typeof x.id === "string") && ("label" in x) && 
    (typeof x.label === "string");
}

const flattenObject = <T extends object>(
  obj: T
): Flattened<T> =>
  Object.fromEntries(Object.entries(obj).map(
    ([k, v]) => isIdLabel(v) ? [v.id, v.label] : [k, v])
  ) as any

Давайте проверим это:

const input = {
  article: 'prova',
  id: 63,
  abc: { id: 'topId', label: 'topLabel' },
  def: { id: 'someId', label: 'someLabel' }
} as const satisfies Input;

/* const input: {
    readonly article: "prova";
    readonly id: 63;
    readonly abc: {
        readonly id: "topId";
        readonly label: "topLabel";
    };
    readonly def: {
        readonly id: "someId";
        readonly label: "someLabel";
    };
} */

const output = flattenObject(input) satisfies Output; 
/* const output: {
    readonly article: "prova";
    readonly id: 63;
    readonly topId: "topLabel";
    readonly someId: "someLabel";
} */

Выглядит неплохо. Тип output — это Flattenсоответствующая версия типа input.

Детская площадка, ссылка на код

Другие вопросы по теме