По аналогии с принципом const formatRubles = (amount: number) => `${amount} ₽`; const amounts = [100, 200, 300].map((amount) => formatRubles(amount));
Можно это написать немного короче: Получается красивый, легко читаемый код без множества скобок. А Typescript делает здесь хорошую работу, проверяя, действительно ли вы можете так сделать с точки зрения типов. Но не всегда. И дальше я разберу два примера: На компонентах React JS — потому что эту проблему легко получить при передаче обработчиков через дерево компонентов. На примере обычных функций. Те, кто работают с React JS, легко представят следующий пример. Допустим, мы сделали визуальный компонент LogButton для кнопки логирования. И используем его в компоненте App в нескольких местах. Я убрал лишнее, чтобы не загромождать код: Обратите внимание, что проп onClick объявлен без аргументов, т.к. мы считаем LogButton бизнесовым компонентом и не хотим выставлять наружу ивент, приходящий из button. При клике по LogButton получаем ожидаемое: Проблема же появляется, когда кто‑то будет рефакторить компонент App и решит сделать «Something happened» значением по‑умолчанию для logConsole. А раз у logConsole теперь есть аргумент по‑умолчанию, а LogButton обещает не передавать в onClick никаких аргументов, то почему бы не передать logConsole в LogButton как объект? Красиво! Но что мы увидим в консоли, вместо «[LOG] Something happened»? Так получилось, потому что button передаёт в обработчик onClick аргумент с описанием события. А Typescript здесь не видит никакой ошибки, потому что позволяет передать в качестве колбэка, как функцию с меньшим числом аргументов, так и функцию с большим числом аргументов (если лишние аргументы являются опциональными). Проследим передачу logClick с точки зрения Typescript. Сначала число аргументов logClick “уменьшилось”, когда она стала пропом для LogButton и это нормально — потому что LogButton даёт обещание, что его onClick не нужно никаких аргументов. А у logClick теперь нет обязательных аргументов (s имеет значение по умолчанию, а значит не обязателен). Затем logClick передаётся в качестве обработчика button onClick, у которого больше аргументов. И для Typescript это тоже нормально (см. FAQ здесь и здесь). Иначе, трюк с formatRubles выше не работал бы, и было бы неудобно — у formatRubles только один аргумент, а map принимает колбэк с тремя аргументами. Решить проблему довольно легко. Например, везде оборачивать функции: Но это не красиво! Хочется понять, где это делать обязательно, а где — нет. Ведь, на самом деле, виноват разработчик компонента LogButton. Он объявил функцию onClick, как не имеющую аргументов (в интерфейсе Props), а передаёт её в качестве обработчика с большим числом аргументов: Т.е. он нарушил контракт своего компонента. Именно ему надо было использовать указанный принцип и обернуть функцию: Потому что кто-то «сверху» мог передать в пропсы функцию с большим числом аргументов. Ещё один пример. Допустим, мы написали странный форматер номеров платёжных карт, который принимает в качестве аргумента функцию, берущую часть строки, и возвращает откуда-то взятый маскированный номер карты: И модуль, который его вызывает и использует упрощённый тип SimpleSlicer для колбэка: Ссылка на Typescript Playground. В консоли будет: Всё хорошо. Но затем кто-то решил отрефакторить функцию sliceV1: Typescript не ругается. В консоль при этом выводится: Безопасность нарушена! Потому что в start вместо ожидаемого значения 15 попал объект { start: 15 } Кто виноват? Автору mistakenFunc следовало обернуть slice, раз он видит, что getCardMask принимает обработчик с большим количеством аргументов: Тогда всё будет хорошо. Соблюдайте контракт и следите, чтобы полученному колбэку передавалось не больше аргументов, чем обещаете. Другими словами: Принимая колбэк с меньшим числом аргументов, оборачивайте его, прежде, чем передавать его далее в качестве колбэка с большим числом аргументов. UPD. Спасибо @Alexandroppolus — я убрал опциональность в типах SliceOptions и SlicerFunc. Действительно, когда объявляешь тип колбэка, то в документации Typescript есть следующая рекомендация: Когда пишешь тип функции-колбэка, никогда не делай параметр опциональным, если только не намереваешься вызывать этот колбэк без этого параметра. Потому что, объявляя колбэк: Мы заставляем пользователя getCardMask обрабатывать кейс, когда options undefined: Хотя getCardMask всегда вызывает slicer-колбэк с этим параметром. Т.е. появляется ненужное усложнение.const amounts = [100, 200, 300].map(formatRubles);
Пример на React JS
// log-button.tsx interface Props { onClick: () => void; } const LogButton: React.FC<Props> = (props) => ( <button className="log-button" onClick={props.onClick}> Log </button> ); // app.tsx export default function App() { const logConsole = (s: string) => console.log("[LOG]", s); return ( <div> <LogButton onClick={() => logConsole("Something happened")} /> {/* Ещё какой-то JSX */} <LogButton onClick={() => logConsole("Something happened")} /> </div> ); }[LOG] Something happenedexport default function App() { const logConsole = (s: string = "Something happened") => console.log("[LOG]", s); return ( <div> <LogButton onClick={logConsole)} /> {/* Ещё какой-то JSX */} <LogButton onClick={logConsole} /> </div> ); }[LOG] { _reactName: 'onClick', _targetInst: null, type: 'click', nativeEvent: PointerEvent, target: HTMLButtonElement, … }// log-button.tsx interface Props { onClick: () => void; } const LogButton: React.FC<Props> = (props) => ( <button className="log-button" onClick={() => props.onClick()}> Log </button> ); // app.tsx export default function App() { const logConsole = (s: string = "Something happened") => console.log("[LOG]", s); return ( <div> <LogButton onClick={() => logConsole()} /> {/* Ещё какой-то JSX */} <LogButton onClick={() => logConsole()} /> </div> ); }// log-button.tsx interface Props { onClick: () => void; } const LogButton: React.FC<Props> = (props) => ( <button className="log-button" onClick={props.onClick}> Log </button> );// log-button.tsx interface Props { onClick: () => void; } const LogButton: React.FC<Props> = (props) => ( <button className="log-button" onClick={() => props.onClick()}> Log </button> );Пример на простом Typescript
interface SliceOptions { start: number; } type SlicerFunc = (s: string, options: SliceOptions) => string; const getCardMask = (slicer: SlicerFunc) => { const userCard = "1234 5678 9012 3456"; const visiblePart = slicer(userCard, { start: 15 }); return `**** **** **** ${visiblePart}`; };type SimpleSlicer = (s: string) => string; const sliceV1: SimpleSlicer = (s) => s.slice(15); const mistakenFunc = (slice: SimpleSlicer) => getCardMask(slice); console.log(mistakenFunc(sliceV1));**** **** **** 3456const sliceV2: SimpleSlicer = (s, start = 15) => s.slice(start); const mistakenFunc = (slice: SimpleSlicer) => getCardMask(slice); console.log(mistakenFunc(sliceV2));**** **** **** 1234 5678 9012 3456const sliceV2: SimpleSlicer = (s, start = 15) => s.slice(start); const mistakenFunc = (slice: SimpleSlicer) => getCardMask((s) => slice(s)); console.log(mistakenFunc(sliceV2));Выводы
type SlicerFunc = (s: string, options?: SliceOptions) => string;getCardMask((s, options) => s.slice(options?.start));
ссылка на оригинал статьи https://habr.com/ru/articles/779458/
Добавить комментарий