Я хочу сгладить объект следующей формы
{
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 недоволен» недостаточно, чтобы это был минимально воспроизводимый пример. Пожалуйста, добавьте комментарии в коде, где выражается недовольство, и точные формулировки любых сообщений, выражающих такое недовольство. • В этом коде много дополнительных/неиспользуемых параметров типа, что делает проблему менее очевидной. Вам нужно, чтобы мы отладили ваш код или просто создали новый работающий код?