Я возвращаюсь к этой проблеме после года, когда был в тупике. Я понятия не имею, как кратко сформулировать этот вопрос, поэтому, пожалуйста, объясните мне.
ПРОБЛЕМА: я хочу сузить тип объединения вложенного объекта по значению его свойства kind
. Однако поле kind
не является обязательным, поскольку по умолчанию оно должно быть "string"
.
Тип TField
(упрощенный):
type TField = {
field: string;
name: string;
placeholder?: string;
required?: boolean;
}& (
| { kind: "string"; value?: string; block?: boolean }
| { kind: "password"; value?: string }
| { kind: "email"; value?: string }
| { kind: "number"; value?: number }
| { kind: "boolean"; value?: boolean; inline?: boolean; toggle?: boolean }
)
// ----- EDIT: I want the "name","field","kind" to be optional
// ----------- By default: "kind" should be 'string'
type PartialBy<T,K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
type TLabeledField = PartialBy<TField, "name"|"field"|"kind">
type TFieldCluster = {
[key: string]: TLabeledField;
};
function makeFields(cluster: TFieldCluster){
const fieldMap = Object.entries(cluster).map(([str, path]) => {
let label = str;
let name,field;
let required = false;
if (label.endsWith("*")) {
required = true
label = label.slice(0,-1)
}
if (label.startsWith("$")){
field = label.slice(1);
name = label.replace(/([A-Z])/g, " $1").toLowerCase();
} else {
field = label // <-- will fix
name = label
}
return [
field,
{
type: "string",
name,
field,
required,
...path,
},
];
});
return Object.fromEntries(fieldMap);
}
const fields = makeFields({
title: {},
subtitle: {kind:"boolean", toggle: true}, // <-- SHOULD BE VALID BECAUSE I SPECIFIED 'KIND'
about: { block: true },
count: { kind:"number", block: true }, // <-- INVALID: 'BLOCK' ISN'T ON NUMBER!
})
Если я попытаюсь исправить все, что упомянул, я получу код по этой ссылке на игровую площадку и... в чем проблема? Где конкретно проблема? Я, должно быть, скучаю по этому.
Я ценю усилия. Я пересмотрю исходный вопрос, уточнив его. Я обнаружил, что Typescript может сузить тип, когда получает правильное значение TField. Проблема возникает, когда я делаю поле «вид» необязательным, и у меня возникают проблемы с написанием кода, который по умолчанию делает его TField «строкового» типа по умолчанию. Я также добавлю все это к исходному вопросу.
Я удалил большую часть кода, чтобы сделать его более кратким. Я пропустил пару мест.
Я настоятельно рекомендую вам поместить свой код в IDE и посмотреть, что произойдет. Когда я это делаю я вижу ошибки, о которых вы не спрашиваете, и в самом коде нет указания, в чем проблема. Он должен прыгнуть прямо на нас. Сказать «У меня проблемы с написанием кода» в комментарии мне не поможет... где конкретно в коде проявляется эта проблема? Это не минимальный воспроизводимый пример , и я не знаю, как действовать без него. Не могли бы вы отредактировать код так, чтобы была понятна проблема?
Протестировано на игровой площадке, и я получаю конкретные ошибки, с которыми сталкиваюсь локально. Исправил исходный пост.
В новой версии ваша проблема в основном связана с тем, что PartialBy<T, K>
не является распределительным по отношению к союзам в T
. Если я исправлю это , как показано в этой ссылке на игровую площадку , то, согласно вашему примеру, все будет работать так, как и ожидалось. Это полностью решает вопрос? Если да, то я мог бы написать ответ; если нет, то что мне не хватает?
Я мог плакать. Да, похоже, это решение. Спасибо.
Радостный крик или грустный крик? Скоро напишу ответ. Пожалуйста, знайте, что я принимаю «да, это, кажется, решение» как подтверждение того, что если я напишу ответ, вы не скажете «на самом деле это еще не все» или что-то в этом роде (я имею в виду, не открывая новый пост с вопросом)
Плач от счастья, лол. И нет, это решает проблему того, что компилятор неправильно сужает подпись. Провел беглый тест локально, но мне этого достаточно. Я ценю ваше терпение
Суть проблемы в том, что, как написано,
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
не распределяется по союзам в T
. Вы явно предполагаете, что PartialBy<T1 | T2 | T3, K>
будет оцениваться как PartialBy<T1, K> | PartialBy<T2, K> | PartialBy<T3, K>
. Но это не так. Ни Выбрать , ни Опустить типы утилит не распределяются по объединениям. (См. Почему дискриминируемое объединение не работает, когда я опускаю/требую реквизиты?) Вместо этого они действуют на объединения сразу, сжимая их до отдельных типов объектов:
Это значит
type TLabeledField = PartialBy<TField, "name" | "field" | "kind">
оценивается как
type TLabeledField = Omit<TField, "name" | "field" | "kind"> &
Partial<Pick<TField, "name" | "field" | "kind">>
что эквивалентно
type TLabeledField = {
placeholder?: string;
required?: boolean;
value?: string | number | boolean;
name?: string;
field?: string;
kind?: "string" | "number" | "boolean" | "password" | "email";
}
и это не то, как ты хочешь TLabeledField
себя вести.
Вы можете взять любой универсальный тип и распределить его по параметру типа, обернув его в распределительный условный тип. Если NonDistrib<T>
не является дистрибутивом, можно написать type Distrib<T> = T extends unknown ? NonDistrib<T> : never
. На самом деле вы не пытаетесь проверить T extends unknown
(намерение состоит в том, что это всегда должно быть правдой, поэтому вы также можете написать T extends any
или T extends T
или что-то еще, всегда верно). Просто это делает тип дистрибутивным.
Это дает вам
type PartialBy<T, K extends keyof T> = T extends unknown ?
Omit<T, K> & Partial<Pick<T, K>> :
never;
И сейчас
type TLabeledField = PartialBy<TField, "name" | "field" | "kind">
оценивается как
type TLabeledField = (Omit<{
field: string; name: string;
placeholder?: string; required?: boolean;
} & {
kind: "string"; value?: string; block?: boolean;
}, "name" | "field" | "kind"> & Partial<Pick<{
field: string; name: string;
placeholder?: string; required?: boolean;
} & {
kind: "string"; value?: string; block?: boolean;
}, "name" | "field" | "kind">>) | (Omit<{ ? */
который расширяется до
type TLabeldField = {
placeholder?: string; required?: boolean; value?: string;
block?: boolean;
name?: string; field?: string; kind?: "string";
} | {
placeholder?: string; required?: boolean; value?: string;
name?: string; field?: string; kind?: "password";
} | {
placeholder?: string; required?: boolean; value?: string;
name?: string; field?: string; kind?: "email";
} | {
placeholder?: string; required?: boolean; value?: number;
name?: string; field?: string; kind?: "number";
} | {
placeholder?: string; required?: boolean; value?: boolean;
inline?: boolean;
toggle?: boolean;
name?: string; field?: string; kind?: "boolean";
} */
и, таким образом, ваш код теперь ведет себя так, как хотелось.
Пожалуйста, отредактируйте , чтобы сделать код минимально воспроизводимым примером , где присутствуют только те проблемы, о которых вы спрашиваете. У нас нет таких типов, как
validatorFn
, или таких значений, какparseLabel
,genFields
илиisEntry
, поэтому наши IDE будут просто жаловаться на это, что в лучшем случае отвлекает. Пожалуйста, отредактируйте, чтобы либо заменить его встроенными типами, либо дать нам определение этих значений и типов (и, если да, используйте обычные имена UpperCamelCase для типов, чтобы отличать их от значений). Кроме того,name
иfield
будут перезаписаны в этом развороте, что является ошибкой TS, о которой вы не спрашиваете. Пожалуйста исправьте.