Я хочу сгладить объект следующей формы
{
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>
(см. предыдущий комментарий) Если вам просто нужна помощь в создании работающего кода, соответствует ли эта версия вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?
Привет и спасибо за ваш комментарий. Я не добавлял ошибки TS, потому что в одной строке их было несколько, и я уже опубликовал игровую площадку, где вам будет легче их увидеть, но сейчас я добавил их в вопрос. Прошу прощения за дополнительные параметры типа: это была часть более сложной функции, и я забыл их удалить. Новый работающий код был бы достаточно хорош, но, к сожалению, на вашей игровой площадке ваш ввод является константой, и у меня нет такой роскоши, потому что тип уже назначен. Есть ли способ обойти это?
Нет, нет пути вокруг этого. если вы комментируете как Input, то TS понятия не имеет, что такое реквизит id, кроме string. Поэтому ответ здесь либо «используйте литеральные типы», либо «это невозможно». Какой из них вы хотели бы увидеть написанным?
Ваша версия с литеральными типами все еще может быть полезна в некоторых случаях, поэтому я бы сохранил ее и отметил, что более обобщенный подход невозможен. Спасибо.






ВАЖНОЕ ПРЕДУПРЕЖДЕНИЕ: если вам требуется, чтобы ввод был аннотирован как тип 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.
• Обратите внимание, что фразы «TypeScript недоволен» недостаточно, чтобы это был минимально воспроизводимый пример. Пожалуйста, добавьте комментарии в коде, где выражается недовольство, и точные формулировки любых сообщений, выражающих такое недовольство. • В этом коде много дополнительных/неиспользуемых параметров типа, что делает проблему менее очевидной. Вам нужно, чтобы мы отладили ваш код или просто создали новый работающий код?