Определение типа

Я возвращаюсь к этой проблеме после года, когда был в тупике. Я понятия не имею, как кратко сформулировать этот вопрос, поэтому, пожалуйста, объясните мне.

ПРОБЛЕМА: я хочу сузить тип объединения вложенного объекта по значению его свойства 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!
})

Пожалуйста, отредактируйте , чтобы сделать код минимально воспроизводимым примером , где присутствуют только те проблемы, о которых вы спрашиваете. У нас нет таких типов, как validatorFn, или таких значений, как parseLabel, genFields или isEntry, поэтому наши IDE будут просто жаловаться на это, что в лучшем случае отвлекает. Пожалуйста, отредактируйте, чтобы либо заменить его встроенными типами, либо дать нам определение этих значений и типов (и, если да, используйте обычные имена UpperCamelCase для типов, чтобы отличать их от значений). Кроме того, name и field будут перезаписаны в этом развороте, что является ошибкой TS, о которой вы не спрашиваете. Пожалуйста исправьте.

jcalz 03.05.2024 05:49

Если я попытаюсь исправить все, что упомянул, я получу код по этой ссылке на игровую площадку и... в чем проблема? Где конкретно проблема? Я, должно быть, скучаю по этому.

jcalz 03.05.2024 05:58

Я ценю усилия. Я пересмотрю исходный вопрос, уточнив его. Я обнаружил, что Typescript может сузить тип, когда получает правильное значение TField. Проблема возникает, когда я делаю поле «вид» необязательным, и у меня возникают проблемы с написанием кода, который по умолчанию делает его TField «строкового» типа по умолчанию. Я также добавлю все это к исходному вопросу.

Julian Brooks 03.05.2024 06:24

Я удалил большую часть кода, чтобы сделать его более кратким. Я пропустил пару мест.

Julian Brooks 03.05.2024 06:25

Протестировано на игровой площадке, и я получаю конкретные ошибки, с которыми сталкиваюсь локально. Исправил исходный пост.

Julian Brooks 03.05.2024 17:15

В новой версии ваша проблема в основном связана с тем, что PartialBy<T, K> не является распределительным по отношению к союзам в T. Если я исправлю это , как показано в этой ссылке на игровую площадку , то, согласно вашему примеру, все будет работать так, как и ожидалось. Это полностью решает вопрос? Если да, то я мог бы написать ответ; если нет, то что мне не хватает?

jcalz 03.05.2024 17:18

Я мог плакать. Да, похоже, это решение. Спасибо.

Julian Brooks 03.05.2024 17:35

Радостный крик или грустный крик? Скоро напишу ответ. Пожалуйста, знайте, что я принимаю «да, это, кажется, решение» как подтверждение того, что если я напишу ответ, вы не скажете «на самом деле это еще не все» или что-то в этом роде (я имею в виду, не открывая новый пост с вопросом)

jcalz 03.05.2024 17:39

Плач от счастья, лол. И нет, это решает проблему того, что компилятор неправильно сужает подпись. Провел беглый тест локально, но мне этого достаточно. Я ценю ваше терпение

Julian Brooks 03.05.2024 17:42
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
10
54
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Суть проблемы в том, что, как написано,

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";
} */

и, таким образом, ваш код теперь ведет себя так, как хотелось.

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

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

Похожие вопросы

Попытка создать уникальную пару матчей для каждой команды, аналогичную формату швейцарской системы Лиги чемпионов УЕФА 2024/25
Угловой 17 | Angular Material 17.3.1: проблема с событиями поля формы Angular Material (фокус) и (размытие)
Реагировать на компонент машинописного текста с условными универсальными реквизитами, неправильно выводить тип
Ввод объекта с одинаковыми ключами, но разными типами их значений
Как динамически выбирать основной цвет в пользовательском интерфейсе Nuxt?
Angular 17 – невозможно привязать RouterLink, поскольку это неизвестное свойство a
Как использовать автономный компонент в неавтономном компоненте?
Ошибка компиляции TypScript. Выражения «await» верхнего уровня разрешены только в том случае, если для параметра «модуль» установлено значение
Пересечения обрабатывают `число` и `логическое значение` по-разному
Как имитировать вызов метода location.back() в Jest для тестирования компонентов Angular?