Обратная совместимость — одно из ключевых требований к современным UI‑библиотекам, которое стоит в одном ряду с требованиями к удобству использования компонентов и наличию качественной дизайн‑системы. Более того, она должна обеспечивать не только сохранение работоспособности проекта клиента после обновления библиотеки, но и неизменность самого подхода к написанию кода. Последний аспект может бросать определённые вызовы для команды разработки UI‑библиотеки и создавать уникальные сценарии.

Привет, Хабр! На связи Павел Урядышев, главный ИТ‑инженер Platform V UI Kit в СберТехе. В этом материале я расскажу, с какой необычной проблемой обратной совместимости столкнулась наша команда во время подготовки релиза UI‑библиотеки Platform V UI Kit. Это решение для построения интерфейсов любого уровня сложности: от корпоративных приложений до сайтов.

О чём речь

О решении для создания интерфейсов любого уровня сложности: от корпоративных приложений до сайтов. Оно состоит из библиотеки React‑компонентов с самой гибкой дизайн‑токен системой и инструментария по её визуальному конфигурированию. Библиотека доступна в open source на GitVerse. Ключевыми инструментами разработки для нас являются React JS и TypeScript.

Библиотека сохраняет поддержку обратной совместимости в рамках минорной версии. Иначе говоря, библиотека гарантирует, что если пользователь обновит её текущую условную версию «1.10.1» на новую условную версию «1.11.0», то:

  • библиотека будет доступна для использования без каких‑либо препятствий;

  • проект пользователя будет корректно работать с уже существующими компонентами библиотеки;

  • пользователь получит доступ к новой функциональности, добавленной в новой версии;

  • в коде не возникнут ошибки типизации TypeScript, которые может вызвать библиотека после обновления.

Если для первых трёх пунктов у команды есть чёткие алгоритмы действий, то выполнение последнего пункта временами требует от разработчиков значительных усилий для поиска правильного решения, которое сможет обеспечить обратную совместимость.

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

Постановка задачи и исходный код

Наша команда получила очередное задание: добавить кнопку очистки значения в правой части текстового поля InputBase. Кнопка будет появляться, если пользователь передаст свойство canClear со значением true, и если поле не пустое. На данный момент код компонента выглядит так:

Исходный код компонента InputBase
Исходный код компонента InputBase

InputBase представляет собой обычное текстовое поле и имеет стандартную реализацию FLUX‑круговорота:

  • Компонент получает значение через свойство value и передаёт его в HTML‑элемент input;

  • При изменении значения в input-е вызывается функция handleChange, которая принимает событие типа React.ChangeEvent<HTMLInputElement> и вызывает функцию обратного вызова onChange из свойства компонента, передавая в неё два аргумента: новое значение поля в качестве первого и само событие в качестве второго.

Решение задачи

Добавляем функциональность очистки поля

Приступаем к реализации: cперва объявим в интерфейсе InputBaseProps новое свойство canClear, которое может являться либо типом boolean, либо типом undefined. Затем добавим саму кнопку очистки и показываем её только если свойство компонента canClear равно true. Перед тем, как отрисовать кнопку, мы проверяем, что значение поля не пустое. Сразу создадим основу функции, которая будет выполнять очистку поля.

Код компонента InputBase c кнопкой удаления значения и будущей функцией-обработчиком
Код компонента InputBase c кнопкой удаления значения и будущей функцией-обработчиком

Теперь нам нужно в функции handleClear вызвать функцию обратного вызова onChange и передать ей пустую строку в качестве первого аргумента, а также event самой функции в качестве второго аргумента. После этого, казалось бы, задача будет завершена!

Исправляем ошибки типизации

Код компонента InputBase c ошибкой типизации при вызове функции onChange в handleClear
Код компонента InputBase c ошибкой типизации при вызове функции onChange в handleClear

Мы не можем просто вызвать onChange в функции handleClear. Если мы попытаемся это сделать, то возникнет ошибка типизации: «Argument of type 'PointerEvent<HTMLButtonElement>' is not assignable to parameter of type 'ChangeEvent<HTMLInputElement>'». Это ожидаемо, так как ранее инициатором события во время изменения значения был HTML‑элемент input, и оно имело соответствующий тип React.ChangeEvent<HTMLInputElement>. Теперь нам нужно учесть, что инициатором события может стать и кнопка удаления, на которую нажмёт пользователь.

Самый простой способ исправить эту ошибку типизации — использовать шаблон TypeScript под названием Union Types, или «Объединение типов», что позволит объединить два типа события в один.

Код компонента InputBase после применения шаблона Union Types
Код компонента InputBase после применения шаблона Union Types

Как видно из 11 и 12 строчек кода, мы объединили типы и теперь аргумент event в функции обратного вызова onChange может быть событием типа либо React.ChangeEvent<HTMLInputElement>, либо React.PointerEvent<HTMLButtonElement>. Это позволило нам устранить ошибку типизации в функции handleClear. Давайте проанализируем наше решение и убедимся, что оно сохраняет обратную совместимость.

Всё ли мы учли?

Представим, что клиент использует компонент InputBase в своём приложении и имеет следующий код:

Код пользовательского приложения с использованием компонента InputBase
Код пользовательского приложения с использованием компонента InputBase

Код представляет из себя форму для подписки на уведомления, в которой есть поле для ввода email и кнопка для отправки данных на сервер. В 11 строчке клиент объявляет функцию handleChangeEmail и передаёт её в свойство onChange компонента InputBase. Обратите внимание, что в 15 строчке он использует второй аргумент event, который имеет тип React.ChangeEvent<HTMLInputElement>.

Что произойдёт с кодом клиента, если он обновит свою библиотеку с нашими последними изменениями?

Код пользовательского приложения с ошибкой типизации после обновления библиотеки @v-uik
Код пользовательского приложения с ошибкой типизации после обновления библиотеки @v-uik

Как видно из 28 строчки кода, после обновления библиотеки у клиента появилась ошибка типизации TypeScript, несмотря на то, что он не изменял свой код. Проблема в том, что теперь функция onChange компонента InputBase принимает второй аргумент в виде объединённого типа React.ChangeEvent<HTMLInputElement> и React.PointerEvent<HTMLButtonElement>. В то же время клиент передаёт в onChange функцию handleChange, где аргумент event соответствует только части типа, а именно React.ChangeEvent<HTMLInputElement>. Чтобы устранить эту ошибку, пользователю нужно будет пройтись по всем участкам кода своего приложения, где используется этот аргумент, и привести его к новому объединённому типу. На основании этого мы с уверенностью делаем вывод, что решение нарушает обратную совместимость библиотеки.

Ищем новое решение

Как можно избежать этой ошибки типизации? Давайте попробуем сосредоточиться на новом свойстве canClear. После обновления библиотеки у клиента это свойство в любом случае по умолчанию будет равно undefined, так как он ещё не использовал его. Нам же так или иначе потребуется шаблон «Объединения типов» для второго аргумента event в функции onChange, по‑другому нам доработку не реализовать. Таким образом, чтобы сохранить обратную совместимость, нам нужно как‑то определить, что если canClear равно true, то мы изменяем тип аргумента event, а если нет — оставляем его прежним.

В итоге наш вопрос для решения задачи обратной совместимости можно сформулировать так: как изменить тип второго аргумента event у функции onChange в зависимости от значения свойства canClear?

В этом нам может помочь шаблон TypeScript под названием Discriminated Union, или «Дискриминантное объединение». Он позволяет TypeScript различать разные типы, основываясь на значении одного общего для них литерального свойства. Такое общее свойство называется дискриминантом.

В нашем случае дискриминантом будет свойство canClear компонента InputBase. Мы будем использовать его значение, чтобы определить тип второго аргумента event в функции onChange. В частности, если canClear равно true, то в аргументе event мы сможем применить шаблон «Объединения типов» для React.ChangeEvent<HTMLInputElement> и React.PointerEvent<HTMLButtonElement>. Попробуем реализовать эту идею.

Для начала переделаем InputBaseProps с интерфейса на тип с общим дискриминантом canClear и разными типами для свойства onChange, в зависимости от значения canClear.

Код нового типа InputBaseProps для компонента InputBase после применения шаблона Discriminated Union
Код нового типа InputBaseProps для компонента InputBase после применения шаблона Discriminated Union

В этом дискриминантном объединении типов мы объявляем, что если свойство canClear равно true, то второй аргумент event в функции onChange будет иметь тип либо React.ChangeEvent<HTMLInputElement>, либо React.PointerEvent<HTMLButtonElement>. Во всех других случаях event будет только типа React.ChangeEvent<HTMLInputElement>.

Снова исправляем ошибки типизации

Однако после этих изменений у нас неожиданно в функции handleClear в 33 строчке возникает новая ошибка, связанная с типами: «Argument of type 'PointerEvent<HTMLButtonElement>' is not assignable to parameter of type 'ChangeEvent<HTMLInputElement>'».

Код компонента InputBase с ошибкой типизации после применения шаблона Discriminated Union
Код компонента InputBase с ошибкой типизации после применения шаблона Discriminated Union

Ошибка возникает из‑за того, что компилятор TypeScript не может чётко определить тип аргумента event. Он не знает, какое значение имеет свойство canClear, поэтому автоматически считает, что event равен типу React.ChangeEvent<HTMLInputElement>. Мы же, в свою очередь, передаём event типа React.PointerEvent<HTMLButtonElement>, что и вызывает ошибку типизации. По нашей задумке canClear должен принимать значение true только в том случае, если event имеет тип React.PointerEvent<HTMLButtonElement>.

Чтобы помочь компилятору TypeScript определить правильный тип аргумента event, мы в функции handleClear применим шаблон под названием Type Narrowing, или «Сужение типов», который позволяет уточнять тип свойств или переменных на основе выполнения условий. Таким образом, используя общий дискриминант canClear, мы будем сообщать компилятору TypeScript, что в данном контексте он действительно будет равен true, и event может быть равен как типу React.ChangeEvent<HTMLInputElement>, так и типу React.PointerEvent<HTMLButtonElement>.

Код компонента InputBase, в котором исправлена ошибка типизации после применения шаблона Type Narrowing
Код компонента InputBase, в котором исправлена ошибка типизации после применения шаблона Type Narrowing

В 34строчке применён условный оператор if для сужения типов. Мы проверяем, что если свойство canClear не равно true, то выходим из функции. Иначе просто вызываем функцию onChange, и тип аргумента event автоматически определяется правильно. Также стоит отметить, что из‑за особенностей компилятора TypeScript мы исключили свойства canClear и onChange из общей декомпозиции на 27 строке и обращаемся к ним напрямую через аргумент props.

Мы точно всё учли?

Давайте вернёмся к коду клиента и убедимся, что все ошибки типизации исчезли.

Код пользовательского приложения, в котором исчезла ошибка типизации компонента InputBase
Код пользовательского приложения, в котором исчезла ошибка типизации компонента InputBase

Как мы видим, ошибки действительно нет. Теперь давайте посмотрим, что произойдёт с кодом клиента, если он передаст в компонент InputBase новое свойство canClear со значением true.

Код пользовательского приложения, в котором появилась ошибка типизации компонента InputBase при указании свойства canClear в значении true
Код пользовательского приложения, в котором появилась ошибка типизации компонента InputBase при указании свойства canClear в значении true

Как и ожидалось, возникла новая ошибка типизации, потому что клиент передаёт неправильный тип аргумента event в свойство onChange, когда canClear равно true. Теперь компонент InputBase будет требовать, чтобы ему передавали объединённый тип React.ChangeEvent<HTMLInputElement> и React.PointerEvent<HTMLButtonElement>, если клиенту понадобится использовать новую функциональность очистки значения поля. Это будет обязательным условием для использования новой функциональности и не нарушит обратную совместимость.

Ещё раз убедимся, что предложенное нами решение является корректным. Мы заменили интерфейс InputBaseProps на тип. Какие могут быть последствия такого изменения? В документации TypeScript указано, что интерфейсы могут взаимодействовать с типами. То есть критических изменений быть не должно. Для наглядности давайте представим ситуацию, в которой клиент решил создать свой собственный компонент Input, основываясь на нашем компоненте InputBase, используя для описания свойств интерфейс.

Предположим, что у клиента есть следующий код компонента:

Код пользовательского компонента Input
Код пользовательского компонента Input

В своём компоненте клиент создал интерфейс InputProps, который расширяет интерфейс InputBaseProps из библиотеки @v-uik. Он добавляет новое свойство description и показывает его под компонентом. Остальные свойства проксируются в компонент InputBase. Что в таком случае произойдёт с кодом клиента после обновления библиотеки?

Код пользовательского компонента Input с ошибкой TypeScript после обновления библиотеки @v-uik
Код пользовательского компонента Input с ошибкой TypeScript после обновления библиотеки @v-uik

После обновления библиотеки у пользователя возникла ошибка TypeScript: «An interface can only extend an object type or intersection of object types with statically known members». Сообщение говорит о том, что пользователь пытается расширить интерфейс с помощью типа, который имеет динамические (не статические) свойства. Проблема заключается в том, что интерфейсы не могут расширять типы, включающие в себя объединённые типы, являющиеся динамическими. Следовательно, такое решение со сменой интерфейса на тип нам не подходит.

Снова ищем решение

Теперь мы понимаем, что нам нужно найти решение, не меняя интерфейс на тип. Если интерфейс не может работать с типами, реализующими дискриминантное объединение, то как решить проблему обратной совместимости по‑другому?

Давайте порассуждаем. Мы знаем, что значение свойства canClear должно определять, какого типа должен быть второй аргумент у свойства onChange в компоненте InputBase. Иначе говоря, свойство должно обладать условным типом. При этом вышеописанный механизм должен уметь работать с интерфейсами TypeScript‑а. Чтобы это реализовать, давайте обратимся к очередному шаблону TypeScript под названием Conditional Types, или «Условные типы». Он позволяет, в зависимости от выполнения необходимого условия, присваивать нужный тип для определённого свойства. Предположим, что реализация будет выглядеть так:

Концепт реализации интерфейса после применения шаблона Conditional Types
Концепт реализации интерфейса после применения шаблона Conditional Types

Если свойство canClear равно true, то свойство onChange будет иметь обновлённый тип, в котором второй аргумент event будет являться объединённым типом. В противном случае мы сохраняем предыдущую типизацию для свойства onChange. Для выполнения этой проверки внедрим в наш интерфейс обобщение (generic), которое будет определять наличие или отсутствие свойства canClear.

Реализация проверки условия присвоения типа с помощью обобщения (generic)
Реализация проверки условия присвоения типа с помощью обобщения (generic)

В 4 строчке мы объявляем обобщение (generic) TCanClear, которое представляет собой булев тип. С 9 строчки мы начинаем проверять его значение: если оно равно true, то мы решаем, что пользователь задал свойство canClear со значением true, и мы можем присвоить свойству onChange новый объединённый тип; а если нет, то canClear не задано (равно undefined) или равно false, и мы оставляем предыдущий тип для свойства onChange. Применённый нами шаблон, в котором мы используем обобщение (generic) вместе с условными типами, называется Distributive Conditional Types, или «Распределённые условные типы», и является частным случаем шаблона Conditional Types.

Последнее, что нам осталось, это связать значение свойства canClear с объявленным нами обобщением (generic) TCanClear.

Код интерфейса InputBaseProps после применения шаблона Distributive Conditional Types
Код интерфейса InputBaseProps после применения шаблона Distributive Conditional Types

В 7 строчке мы присвоили свойству canClear наше обобщение TCanClear. Этот способ позволяет компилятору TypeScript автоматически определять тип свойства canClear в зависимости от контекста присвоенного пользователем значения. В частности, если canClear будет равно true, то это значение присвоится обобщению TCanClear. Это запустит нужную проверку типа для свойства onChange в 9 строчке. Подход, который мы применили, является очередным шаблоном TypeScript под названием Contextual Typing, или «Контекстная типизация».

Давайте применим наш новый интерфейс в компоненте и посмотрим, как изменился наш код.

Код компонента InputBase после изменения применения шаблонов Distributive Conditional Types и Contextual Typing
Код компонента InputBase после изменения применения шаблонов Distributive Conditional Types и Contextual Typing

В 33 строчке мы столкнулись с новой ошибкой типизации. Сообщение говорит о том, что мы не можем передать второй аргумент event в функцию onChange, потому что он не соответствует ожидаемому типу React.ChangeEvent<HTMLInputElement>. На самом деле, сейчас тип этого аргумента — React.PointerEvent<HTMLButtonElement>. Как мы можем это исправить?

Во‑первых, нужно понимать, что в текущей реализации компилятор TypeScript не может применить шаблон «Сужение типов». Наличие условного оператора if в 29 строчке нам никак не поможет. Во‑вторых, обобщение (generic) TCanClear в нашем компоненте никак не фигурирует. Поэтому нам нужно добавить это обобщение (generic) и изменить тип свойство onChange на правильное. Давайте выполним эти шаги.

Код компонента InputBase с исправленной ошибкой типизации
Код компонента InputBase с исправленной ошибкой типизации

В 21 строчке мы добавили обобщение TCanClear и передали его в наш интерфейс InputBaseProps. Предполагая, что пользователь присвоит свойству canClear значение true и будет использовать функциональность очистки поля, мы создаём временную переменную _onChange и задаём ей новый тип с изменённым вторым аргументом event. Затем в 42 строчке мы просто её вызываем.

А сейчас мы действительно всё учли?

Давайте снова вернёмся к коду первого клиента, который использовал наш компонент в своём приложении, и убедимся, что типизация нашего компонента работает так, как ожидается.

Код пользовательского приложения, в котором нет ошибки типизации компонента InputBase с незаданным свойством canClear
Код пользовательского приложения, в котором нет ошибки типизации компонента InputBase с незаданным свойством canClear

Действительно, ошибок в коде у клиента нет. Давайте теперь клиент попробует передать свойство canClear в компонент InputBase.

Код пользовательского приложения, в котором появилась ошибка типизации компонента InputBase после указания свойства canClear со значением true
Код пользовательского приложения, в котором появилась ошибка типизации компонента InputBase после указания свойства canClear со значением true

Ошибка типизации появилась, так как клиент передаёт в свойство onChange функцию с неправильным типом второго аргумента event, как и ожидалось. Теперь вернёмся к коду второго клиента и убедимся, что после обновления библиотеки у кода его пользовательского компонента не появились ошибки типизации.

Код пользовательского компонента Input с отсутствующей ошибкой TypeScript после обновления библиотеки @v-uik
Код пользовательского компонента Input с отсутствующей ошибкой TypeScript после обновления библиотеки @v-uik

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

Заключение

В ходе нашей работы над проектом мы создали решение, которое полностью соответствует принципам обратной совместимости. Это означает, что мы внедрили новую функциональность очистки поля, не нарушая существующий код клиентов. Такое внимание к обратной совместимости очень важно, так как оно позволяет пользователям безболезненно интегрировать обновления и новые возможности в свои приложения с помощью нашей библиотеки.

Одним из ключевых факторов, способствующих сохранению обратной совместимости, стало использование шаблонов TypeScript. Благодаря им мы смогли сделать код более гибким и безопасным, что позволило избежать проблем у клиентов при обновлении библиотеки.

Если вас заинтересовал наш подход и вы хотите ознакомиться с полным кодом нашего компонента, то он доступен на платформе GitVerse.

Комментарии (10)


  1. artptr86
    18.12.2024 12:54

    Я бы посоветовал во всей статье избавиться от термина "шаблон", потому что ничто из перечисленного шаблонами ни в смысле pattern, ни в смысле template, ни даже generic, как можно было бы подумать, не является.


    1. FedExRU Автор
      18.12.2024 12:54

      Да, я понимаю про что ты. При написании статьи я долго думал, как подходы TypeScript (условное объединение, и т.д) можно объединить в одно ёмкое название. Паттерн, подход... В итоге я решил предложить вариант "Шаблон", так как он чаще всего мне попадался на глаза во время поисков. Других альтернатив на просторе документации Typescript или русскоязычного коммьюнити я не нашел.


      1. artptr86
        18.12.2024 12:54

        Вообще это средства работы с типами. Можно жаргонно назвать их фичами системы типов (type system features). Ну или инструментами. Иначе это звучит как: "Для нахождения суммы чисел мы применим такой шаблон, как сложение". Глупо же, сложение — это не шаблон, а операция. Так и механизмы для работы с типами — в данном случае не шаблоны.


        1. FedExRU Автор
          18.12.2024 12:54

          Точно так же, как и операция - это один из механизмов работы с числами.

          Проводя аналогию, если простое объединение типов можно было бы назвать "оператором", так как используется один символ `|`, то в случае с распределенными условными типами, где нужно указание дженерика, условных операторов с ключевыми словом `extends` и т.д - уже сложно называть оператором. Думаю, ты меня понял.

          Мой тейк, в данном случае, это предложить русскоязычную адаптацию понятия "фича", которую ты упомянул. Использование слова "фича", по сути, является транслитерацией, все равно что вместо "функции обратного вызова" писать "кАлбэк" или вместо "свойства" - "проп".


          1. artptr86
            18.12.2024 12:54

            Ну вот нельзя слово feature перевести словом "шаблон" ну никак. Вот "механизм" или хотя бы "возможность" были бы более подходящими. Говорят же "механизм наследования", "механизм обобщённых типов".

            Кстати "тейк" и "дженерик" — тоже транслитерации :)


            1. FedExRU Автор
              18.12.2024 12:54

              Ну почему нельзя? В англоязычном коммьюнити повсеместно и, достаточно, устойчиво используется слово "Pattern". Почему бы не перевести это, как шаблон? Особо проблем, лично я, тут не вижу. "Механизм" и "возможность" - слишком абстрактные понятия, как по мне, чтобы они давали хоть какой-нибудь намек на то, что они делают. "Шаблон" же дает некий намек на то, что это какая то предзаложенная последовательность действий.

              Кстати, я не против транслитерации в лексиконе) В русскоязычной документации это всегда смотрится неуместно, как по мне)


              1. artptr86
                18.12.2024 12:54

                В англоязычном коммьюнити повсеместно и, достаточно, устойчиво используется слово "Pattern"

                Да, но в каком контексте используется это слово? В программировании у него семантика "шаблонное архитектурное решение". Например, шаблон "Стратегия" реализуется посредством наследования классов реализации стратегии от общего интерфейса. Шаблон "Команда" тоже реализуется наследованием реализации от интерфейса. Но смысл в этих шаблонах разный. При этом саму возможность и механизм наследования никто шаблоном не называет.

                Так же и в документации Typescript встречается слово pattern, но оно используется, в частности, для описания миксинов. Сам по себе typescript не содержит готовой возможности для работы с миксинами (у него нет ключевого слова mixin или чего-либо подобного), но такой архитектурный подход возможен, и для него естественно использовать слово "шаблон".

                В вашем же случае шаблонами можно было бы назвать не сами механизмы "Объединение типов", "Дискриминантное объединение" и т.п., а конкретные архитектурные подходы работы с объектами событий. Конечно, здесь вводить терминологию будет уже сложнее.


                1. FedExRU Автор
                  18.12.2024 12:54

                  В TypeScript нет общепринятой терминологии для этого, что создает путаницу в названиях. Программирование - это обширное понятие, а типизация - всего лишь небольшая его часть, собственно, и сама статья посвящана этой части. Называть это "шаблоном типизации" вполне нормально, и это не звучит слишком вычурно или сложно.

                  "Шаблон стратегия" или "Шаблон Команда" - это же не про Typescript, это про шаблоны программирования, и да, там официальное название - это Pattern.

                  Если мы говорим про Typescript - "шаблон" или "шаблон типизации" - звучит вполне логично, потому что это тоже паттерн для реализации нужного итогового поведения в результате типизации (например, компонента).

                  В зависимости от разных контекстов одинаковые слова имеют разный смысл, и это нормально, если, базово, они будут звучать одинаково.

                  Пример (очень утрированный): слово "Запрос". Если мы говорим в контексте Web - запрос (query) - это отправка "сообщения" на сервер. В контексте баз данных - это команда с нужными операциями, отправляемая к базе данных. (Запрос к серверу и запрос к базе данных - это все программирование).


  1. Alexandroppolus
    18.12.2024 12:54

    Вопрос - точно ли надо в onChange дополнительным параметром передавать event? Да, у вас там был пример, где вызывается event.preventDefault, но preventDefault не работает для этого события. Не говоря уж о том, что это управляемый компонент, и value можно менять (или не менять) по своему усмотрению.

    Обычно используется один вариант из двух: либо в onChange передают только event, чтобы кастомный инпут был по пропсам как нативный, либо передают сразу value, это удобно, например, с обычным useState - можно написать onChange={setValue}. Либо ещё делают сразу два события - onChange и onChangeValue, с евентом и строкой соответственно.

    Если всё-таки прямо нужен event, то всё равно в обработчике onChange абсолютно без надобности PointerEvent. Ну вот реально, зачем мне координаты клика, очистившего поле ввода? Можно вызвать изменение текста программно, у нас для этого используется такая утилита, способная правильно триггернуть нужное событие (которое, в свою очередь, вызовет реактовское onChange):

    export const setInputValue = (
        inputElement: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null | undefined,
        value: string,
    ): void => {
        if (!inputElement) {
            return;
        }
    
        const valueSetter = Object?.getOwnPropertyDescriptor(inputElement, 'value')?.set;
        const prototype = Object.getPrototypeOf(inputElement);
    
        const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set;
    
        if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
            prototypeValueSetter.call(inputElement, value);
        } else {
            valueSetter?.call(inputElement, value);
        }
        inputElement.dispatchEvent(new Event('input', {bubbles: true}));
    };


    1. FedExRU Автор
      18.12.2024 12:54

      Спасибо за комментарий) Сейчас постараюсь все разъяснить:

      Вопрос - точно ли надо в onChange дополнительным параметром передавать event?

      Да, в большинстве случаев такой подход избыточный. API наших полей ввода как раз таки принимает первым аргументом value , чтобы само значение можно было быстро и удобно получить без прямого обращения к объекту события.

      Да, у вас там был пример, где вызывается event.preventDefault, но preventDefault не работает для этого события. 

      Тут пример чисто демонстрационный (может быть и не самый удачный), который показывает, что пользователи могут как-то завязаться в своих функциях-обработчиках именно на второй аргумент event , который они предварительно затипизируют в соответствии с типами самого компонента. И что если мы как-то рискнем поменять тип второго аргумента, то это уже не будет сохранять обратную совместимость в рамках минорного релиза библиотеки, и нам придется выносить всю доработку в мажорную версию.

      Если всё-таки прямо нужен event, то всё равно в обработчике onChange абсолютно без надобности PointerEvent.

      Изначально, как мы рассчитывали, в рамках компонентной библиотеки все таки есть потребность в разделении сущностей, которая вызвала изменение поля: либо изменено с помощью нативных механизмов поля input, либо изменено с помощью стороннего компонента (при нажатии на кнопку очистки поля).

      Ну вот реально, зачем мне координаты клика, очистившего поле ввода?

      Тут как раз, да, речь о том, кто именно поменял значение поля, если прилетел PointerEvent , то это кнопка очистки поля.

      Можно вызвать изменение текста программно, у нас для этого используется такая утилита

      Да, это отличное решение! У нас, в свою очередь, тоже есть подобная утилита для генерации событий, привязанных к полю. Мы тоже начали смотреть в эту сторону, чтобы на программном уровне генерировать события, и отходить от разделения типов объекта event , чтобы он всегда мог иметь один тип.

      Надеюсь, смог ответить на ваш вопрос)