Comments 22
А что-то вроде:
type GetUnreadMessages =
| { groupId: string; }
| { chatId: string; }
| { userId: string; }
не подойдет?
Такой тип "пропустит" объект, в котором заполнено сразу несколько полей. Чтобы разрешить ввод значения строго только для одного поля из трех, нужен примерно такой тип:
type GetUnreadMessages =
| ({ groupId: string } & Partial<{ chatId: undefined, userId: undefined }>)
| ({ chatId: string } & Partial<{ groupId: undefined, userId: undefined }>)
| ({ userId: string } & Partial<{ groupId: undefined, chatId: undefined }>);
Что то подобное автор и конструирует автоматически.
В дополнение к предыдущему комменту (кстати, этот момент описан в статье), еще добавлю, что мы используем автотипы из сваггера, то есть начальный тип уже у нас есть. Нам нужно только его переработать под конкретные нужды.
Когда-нибудь, когда сваггер начнет адекватно размечать такие типы, заживем =)
typescript творит чудеса, но меня все же напрягают такие решения, потому как, а разберется ли кто то в этом после меня, да и разберусь ли я в этом сам спустя время? Тем более брать такие готовые рецепты. Слишком они загадочны.
Я поначалу тоже копировал со stackoverflow примеры, потом через пару спринтов я уже не мог вспомнить что это за хрень такая и как я смог родить такое. Приходилось опять гуглить. Пример с union в комментариях все же много понятнее, хоть и не универсально. Да и такое решение проще в коде найти и видно, что оно делает с первого взгляда.
Есть такая тема, именно из-за нее в конце статьи есть тесты на типы XD)
Если практически отвечать на этот вопрос, то такие типы не требуют поддержки, а использование в коде выглядит довольно прозрачно, ex
type RequestPayload = RequireOnlyOne<SwaggerRequestPayload>;
В целом, с перого раза понятно, что делает этот код, а тесты позволяют быть уверенным, что он выполняет свою работу
До этого не доводилось встречать тесты типов как то. Благодарю за пример. Хороший код. Думал об этом, но руки как то не доходили посмотреть.
Да, тесты сильно добавляют понимания + помогут в поддержке через месяц. Еще понимания добавляет выделение утилитарных типов (промежуточных), как вы и делаете, а не все писать одним комбайном. В общем - сделать такие вещи понятными и поддерживаемыми явно можно и не сложно.
Ругается же)
ts: 5.2.2
Это из-за объектного литерала - ts из коробки не допускает в нем лишних полей. Здесь на самом деле должен быть такой код:
type TypeWithGroupId = {
groupId: string;
}
const value = {
groupId: '1',
chatId: '2'
};
// тут не ругается, а нам надо чтоб ругался
const payload: TypeWithGroupId = value;
Пример заменил, спасибо =)
Я когда вижу такие решения, у меня только один вопрос: вот это вот всё (~30 sloc и тройная вложенность <>
) – чтобы что?
Весь хабр забит статьями в духе не мудри, не выпендривайся, читаемость почти всегда важнее... Но на мир TS это как будто не распространяется, тут наоборот доблесть навертеть похитрее.
Чем плохо вместо трех опциональных полей сделать тупо и понятно?
type GetUnreadMessages = {
entityType: ET_GROUP | ET_CHAT | ET_USER;
entityId: string;
}
Поскольку это преобразование надо делать для кучи исходных типов, всё равно придется запилить type-util, который будет выглядеть примерно так же, как представленная здесь реализация. A потом везде вместо очевидного {chatId: '1'}
писать дурацкое {entityType:'chatId', entityId:'1'}
Так не дурацкое же, а наоборот логичное.
Потому что этот entityType явным образом говорит, что мы хотим получить. Условно говоря, в какую таблицу базы нужно сходить. А три взаимоисключающих айдишника сообщают то же самое, но неявно - на той стороне потом нужно прогнать проверки в духе if groupId != null else if chatId != null else... Это хрупкая и неудобная система, но зато с модными типами.
Ну ок, о вкусах спорить не буду. Однако, модный type-util для этого вашего entityType написать всё равно придется, ведь так? Потому что в исходно заданных типах его нет
В этом случае придётся бекенд перелопачивать. Но из плюсов тогда на сваггере можно корректно описать такой тип, ибо сваггер умеет дискриминировать типы по значению какого либо поля.
Так по методу в статье это будет короче выглядеть:
type NewType = RequireOnlyOne<GetUnreadMessages>
И все понятно. Остальное будет кодом инфры. Который не нужно менять при добавлении поля.
Плюс тут типизация более естественная, а вы делаете сурогатные поля - у сущностей нет entityType и entityId.
И вы накладываете жесткие ограничения на тип GetUnreadMessages
(его структуру). А кто вам сказал, что тип GetUnreadMessages
можно менять и он может быть другим? Кто сказал, что там только айдишники? А если там или ордерАйди или юзерНейм, нужно новые имена сурогатным полям? А если завтра добавится продуктСКУ, вы будете везде переименовывать и называть еще как то более дженерик именами ваши поля?
Автор выходит из того, что есть вот такая вот ситуация и конкретный формат входного типа. И показывает как это можно решить + гибко, что бы не дописывать при добавлении полей, что важно.
Вы меняете условие (меняете входной формат) и предлагаете другое решение. Ваше тоже хорошее и пользвал много раз. Оно простое и не требует никакой подготовки инфры. И наверное более стандартное. Короче протоптанная тропа, это ясно. Но это решение немного не той задачи, что автор решал.
Но хорошо, что вы указали на этот кейс. Что бы люди понимали, что когда все просто - решения нужно брать простые. И когда можно брать простые. Решение автора - это больше для фреймворков, кода инфры (если большая своя) или для команды с большим опытом ТСа. В остальных случаях скорее всего люди будут брать ваше решение или юнион как в первом комменте к статье.
у сущностей нет entityType и entityId
Я не согласен. Уже когда мы сказали, что сущность может быть группой, чатом или юзером, мы уже подразумеваем, что у неё есть тип из этого списка. Вопрос только, введем ли мы его явно или будем извлекать из косвенных данных? Я за явное: либо вводить поле, как я написал, либо разделять три перечисленных объекта на три отдельных типа (а там где нужно, будет их юнион).
То есть дело тут не только и даже не столько в громоздких инфраструктурных типах, а ещё в дилемме явное-неявное.
Единственное оправдание, с которым я готов согласиться - так уже сделано раньше и нужно поддерживать консистентность с легаси-кодом.
Согласен про явное-неявное. Самому хочется всегда все сделать четко и все границы видимыми. Мне просто способ с типами кажется не таким уж неявным. Но тут наверное уже дело привычки и вкусов в команде.
И такая ситуация не только в легаси может быть. Например ивенты от разных систем (на других языках, рантаймах, нодах, ...), а вам их обрабатывать. В общем везде, где мы не контролируем саму получаемую сущность. В различных журналах это часто.
Также можно использовать чисто внутри приложения - типа выбор фильтра для поиска, ивенты, ... Там где вы все проверите ТСом и весь ТС ваш и команде ок.
Еще во всякого рода фидах (типа главная страница/лента в соц. сетях), где сущность разного типа прут в одном листе, т.е. где естественна неоднородность отдаваемых сущностей.
Еще где я хочу сделать внешний АПИ и мне незачем наваливать пользователю еще один параметр. Я говорю ему: или групАйди или юзерАйди. Больше ничего. Не нужно никаких тайпов. С точки зрения юзера АПИ - так проще. Меньше шума. Имя филда делает разделение типов. Ему понятно все. Если для меня это важно - то способ из статьи лучше. Это фактически и приведено в статье.
Но если это способ общения со своим же Java REST, то большинство скорее всего сделает вашим способом.
В общем я к тому, что такое бывает. Это не самый популярный кейс, но бывает. Ваш - более стандартный и частый.
Спасибо. Я бы тоже так сделал.
Тут ответ несколько проще - это внешнее апи, которое вы не контроллируете. Хотите перелопатить им бекенд - вперед, это тоже путь. Но кому-то, возможно, нужно более быстрое решение, к тому же, не прям уж оно сложное (все утили вместе с тестами улеглись в 30 строк!)
Тоже склоняюсь к этому варианту больше. Кажется немного оверхедным пилить вариант предложенный автором.
Сервис с тремя публичными и одним приватным методами. Приватный делает запрос. Публичные получают стрингу на вход. Приватный - два параметра, тип айди и его значение.
Намудрили слишком. Проще нужно быть и люди к вам потянутся.
Проверки, что ничего не сломается предоставить тестам.
Рецепты Typescript: выбор одного и только одного обязательного поля в объекте