Comments 34
TypeScript - безусловно необходимый инструмент, без которого уже нельзя представить современную разработку. Но автор явно перегибает палку, городя огороды там, где им не место. Начиная с первого примера: мы хотим убедиться, что в коде функции не кириллица? Ок, описываем тип. Как нам теперь убедиться что в описании типа нет ошибки? Там что, не может быть кириллицы?
Ваш метакод - это тоже код. Чем его больше и чем он сложнее - тем больше и вероятность ошибиться. В итоге, мы получили громоздкого монстра там, где могли бы обойтись одной строчкой легко читаемого кода и не сократили вероятность ошибки, а повысили. Также, мы повысили когнитивную нагрузку на работу с кодом, потратили лишнее время... И это там, где проблемы и не было изначально, так как подобные вещи легко тестируются и дебажатся и ловятся спелчекером.
Хороший признак профессионализма - умение упрощать а не усложнять.
Подобный пример служит для демонстрации крайне малозаметной ошибки. Вероятность такой ошибки не имеет значения для учебного примера.
В жизни, к сожалению, ошибки куда более изощренные, но для них нужен более сложный контекст, который скорее перегружает читателя.
На написание типов действительно может уходить существенное время, но при правильном подходе - это окупается со временем. Программист решает окупится ли для него написание типов. Для простой локальной функции - скорее не окупится, для функции с множеством потребителей и повышенными требованиями к надежности - скорее да.
Вероятно, вы плохо поняли мой коммент. Вероятность ошибки повышает увеличение общего объема и сложности кода. Всегда.
А в приведенном примере просто нет смысла, так как проверка верности декларации в одном месте делается за счет такой-же дополнительной декларации в другом месте. Это, примерно, как сцепить два навесных замка и утверждать что теперь надежность и безопасность ворот повысилась, при том, что ворота откроются если вы взломаете любой из замков.
Что автор, на мой взгляд, упустил я отметил ниже. Но и вы, на мой взгляд, совершили туже ошибку.
Начиная с первого примера: мы хотим убедиться, что в коде функции не кириллица? Ок, описываем тип. Как нам теперь убедиться что в описании типа нет ошибки? Там что, не может быть кириллицы?
Мы не хотим убедиться, что в коде функции нет кириллицы. Мы хотим описать все доступные юниты измерения. И потом в программе использовать только такие юниты. Иными словами мы добавляем сущность в направлении которой будут идти зависимости.
Так как эта сущность у нас одна в одном месте, мы легко можем ее протестировать. На тип вы так же можете написать тест, что бы кто-нибудь когда-нибудь потом не совершил какую-нибудь опечатку.
Или, т.к. это место у нас локализовано просто сами можете сделать такую проверку, что бы сэкономить время и не писать тест.
Действительно, типы - это такая же кодовая база как и любая другая, поэтому именно задача определяет какой объем типов нужно реализовать.
Вы говорите, что проблемы не было изначально? Ну, оказывается была, просто вы ее делегировали спелчекеру. Т.е. проблемы то есть, просто решения у вас с автором разные. Но если копнуть глубже, автор вообще не это имел в виду, скорее всего.
Еще недостатки Typescript: 1) когда Вы пишите абсолютно новое то Вы не всегда четко знаете предметную область и соответственно Вы априори не можете определить типы изначально 2) Когда Вам надо внедрить в проект js код то Вы обязаны его приводить к системе Ваших типов но сторонняя библиотека об Вашей иерархии абсолютно ничего не знает. Встает вопрос - а чем тогда отличается подход обьектного проектирования(программирования) от подхода первоначального определения типов ? Явное требование первоначально все "затипизировать" часто просто мешает ходу проектной мысли И тут точно нужен некий баланс
Это все отговорки. Тип чаще всего известен и ничего не мешает его определить.
мешает ходу проектной мысли
Бред, полет фантазии это не про программирование.
Не угадать с типами действительно можно - это, конечно, вопрос к постановщику задачи, но не будем о грустном 🥲
Однако, даже в такой ситуации можно достаточно легко расширять типы.
Например, как показано в примере, сначала от вас требовали значения только в числах, потом, представим, потребовали добавить поддержку процентов, вы расширили тип и благодаря IDE сразу же увидели в каком месте закралась ошибка без отладки.
К сожалению, на словах это сложно принять, поэтому я настоятельно рекомендую вам именно попробовать написать что-то в таком стиле чтобы почувствовать, так сказать, на кончиках пальцев 😉
Добавим поддержку значений в px и в %
Ширина ладно. В реальности там намного больше возможных единиц, но их нетрудно перечислить. Опустим сейчас всякие auto и max-content, хотя с ними есть вопросы.
А вот допустим в CSS есть несколько форматов цветов, у каждого по нескольку вариаций форматов параметров и корректно их все учесть - задачка нетривиальная.
Тоже предлагаете всё типизировать?
P.S. Лично я ограничился бы двумя базовыми случаями - число и строка. Достаточно ловить всякие ошибки с андефайнами и функциями вместо их вызова. А если кто-то передал некорректную CSS-строчку, то сам себе буратино. Проще ловить такие опечатки при тестировании, чем иметь и поддерживать горы запутанного (а он почти всегда такой) инфраструктурного TS-кода.
Как и во всяких других ситуациях, смотрим соотношение усилий и полезного выхлопа.
Насчёт цвета - у нас, например, все цвета лежат в теме (типизированная тема для styled-components), так в коде "цвет" обычно задаётся как ключ из объекта темы.
в коде "цвет" обычно задаётся как ключ из объекта темы
В бизнес-коде - возможно, но автор рассматривает библиотечный пример, где всегда ценится гибкость. Библиотечный компонент, куда нельзя передать простой литерал (или переменную, содержащую литерал) - это прямо очень странно и вряд ли кому-то нужно. Собственно, типизации литералов первая половина статьи и посвящена.
Поэтому нет, одними только ключами тут не обойдемся. Цветов много всяких и большая часть из них действительно используется. Чтобы всё это типизировать, нужно прямо очень сильно заморочиться, а потом поддерживать эту машинерию по мере развития CSS (недавно вот ещё относительные цвета подвезли, там вообще всё сложно).
Насчёт произвольных цветов, конечно, соглашусь - надо просто оставить строку. Да и не уверен, что при всём желании их можно было бы полностью протипизировать - только для 16-ричного формата понадобится безумный юнион из 16^6 элементов, а в тс есть лимиты на размер.
Ну вот мне кажется, что с размерами ситуация похожа.
Ведь автор фактически на типизацию попутно ещё возлагает функцию валидации строковых значений. Если это правда необходимо, то проще и удобнее делается в рантайме, а не типами (причем в коде автора валидация в рантайме всё равно частично присутствует, чистыми типами не обошлись).
С размерами, кстати, будет забавный превед, если понадобится использовать css-переменные и/или calc. Это, кажется, хоронит всю идею.. Иначе подход имел бы право на существование. Затащить библиотечку с такими требованиями сложновато на проект, где размеры в строках, это да. Но не так трудно использовать это у себя по всему проекту. Откуда берутся значения для размеров? Это либо готовые литералы, либо шаблонные строки с числом - и то и другое укладывается в тип Width
. В общем, тут дело вкуса. Народ вон иногда прикрепляет к числам "теги", чтобы потом длину с шириной не путать)
Я если делаю свойства, которые потом напрямую передаю в CSS, вообще с типом не заморачиваюсь, просто string и все. А в описании пропса пишу, валидное значение для такого-то свойства. Просто если в css будет передано что-то невалидное, оно просто не будет работать, но нечего больше не поломается, так зачем какие-то сложные типы.
Ширина ладно. В реальности там намного больше возможных единиц, но их нетрудно перечислить. Опустим сейчас всякие auto и max-content, хотя с ними есть вопросы.
Конечно, в реальном коде вариативность может быть существенно больше (или меньше), но для демонстрации механики достаточно, что типов просто больше чем один.
А вот допустим в CSS есть несколько форматов цветов, у каждого по нескольку вариаций форматов параметров и корректно их все учесть - задачка нетривиальная.Тоже предлагаете всё типизировать?
Нет, с чего вы взяли? Не все строки необходимо типизировать, некоторые действительно стоит оставить просто строками без дополнительных деталей как в примере что вы привели.
А если кто-то передал некорректную CSS-строчку, то сам себе буратино.
Таким образом вы не решаете проблему, а переносите ее на чужие плечи, чего на мой взгляд, можно легко избежать. Если вам известно, что ваша функция поддерживают значения определенных типов, то вы можете ограничить ее использование при помощи типов исключив проблему передачи неправильных аргументов как таковую.
Стоит сделать оговорку, что ограничения не должны быть избыточными, но это отдельный большой вопрос как этого можно добиться.
вы не решаете проблему, а переносите ее на чужие плечи
Переношу, но не на чужие, а на того, кто и должен за этим следить.
В данном случае это правильно по целому ряду причин:
1. CSS уже довольно богатый язык, разнообразие значений и их сочетаний очень большое. Типизация будет очень сложной и наверняка всё равно неполной. Я хочу использовать некое сочетание свойств, а мне не дают, потому что такой кейс не учли.
2. При этом некорректное значение в стилях обычно ничего фатально не ломает, приложение не падает.
3. Опечатки в стилях и без того ловятся гораздо проще, чем смысловые ошибки (девтулзы подсвечивают).
4. В целом попытки "полноценного" решения этой проблемы выглядят раздуванием из мухи слона.
Что это за правильный код ТС, где вы используете кастинг, возвращаете в match пустой массив, и чтобы это пофиксить следующей строкой дефолтите его на 0px..
Кастинг, к сожалению, иногда приходится использовать для строк в рантайме, так как встроенные функции такие как match не знают ничего о типе литерала, что ей передали. Это можно обойти, но для учебного примера на мой взгляд - это перегрузка читателя. Не обязательно во что бы то ни стало типизировать воообще все.
Пустой массив возвращается из match так как match может вернуть null, тип null исключается путем добавления заглушки в виде пустого массива чтобы не писать в дальнейшем проверку на null. Это просто предпочтение, не ищите глубоких смыслов.
Дефолтные значения нужны для того же для чего и всегда - показать какие значения будут, если не будет что-то найдено (или задано) явно. Тут тоже без глубоких смыслов.
Лично для меня утиная типизация в TS, помимо её плюсов, заставляет быть бдительным в части её минусов. Объясню. Вот ждёт фронт с бэка данные аккаунта. Ник и имэйл. На бэке мы получаем ентити из БД, в которой имеется и пароль тоже. И TS с удовольствием пропустит данное поле вместе с ожидаемыми. Каждый раз приходится обрабатывать, исключая ненужные. P.S., может быть я просто не знаю как это готовить.
Не понятно в чем проблема в данном случае. Если в том что пароль приходит на фронт то система очевидно дырявая уже на беке который этот пароль отправил, тут тс не виноват. Если в том что это где-то помешает функционировать программе то это достаточно редкий случай, всё-таки стоит на тс писать с учётом того что в объекте может лежать что угодно ещё кроме того что позволяет тип, но в целом это конечно недостаток.
Готовить это проще всего в описанном случае через code first автогенерацию клиента апи на фронте из кода на беке, тогда такие случаи будут исключены. Или делать design first чтобы было больше возможности подумать над тем что отправляете.
А если бэк тоже на TS? И типы для фронта и бэка общие. Монорепо. Об этом говорю. Бэк отправит и пароль тоже.
Не очень понял тогда в чём здесь недостаток именно тс. Если в дто есть пароль, а его там при этом быть не должно, это просто ошибка дизайна дто. На любом языке так будет.
Утиная типизация позволяет языку оставаться гибким, не требовать избыточной строгости, утомительного приведения типов - это скорее плюс.
В вашем случае, думаю, проблема на стороне архитектуры, так как она почему-то передает по сети приватные данные.
Обычно в таких случаях упаковывают DTO так или иначе, который знает что ему надо содержать, а все лишнее вырезают даже если их туда пытались запихать.
Более строгая типизация в TS может быть достигнута через классы, обратите внимание как устроен NestJS (фреймворк для бэкенда).
Читая статью, а потом комментарии, вижу, что корень проблем в большинстве случаев общий. Причем даже в статье ровность повествования в один момент выбивается из колеи, так как не учитывается данное положение. Поэтому могу предложить информацию к размышлению.
Постановка задачи.
Допустим, мы разрабатываем компонент, который должен корректно обрабатывать ширину элемента так же, как это сделано, например, в компоненте VCard из Vuetify:
Я перешел по ссылке VCard, и там нет никаких %
и px
. Поэтому неплохо было бы задавать задачу не через пример, а явно указывая, что хотите сделать.
Начнем с привычного подхода — просто напишем код без явного указания типов:
Один тип вы всё-таки задали. И если так со стороны посмотреть, проблема тут не в инферинге, так как инферится всё корректно. А в том, что вы реализуете несколько другую задачу, которая отличается от неназванной вами задачи.
TypeScript не подсвечивает ошибку, потому что он не понимает, какие данные мы ожидаем. В результате неправильное значение спокойно проходит, и даже опечатка остается незамеченной.
ТС не подсвечивает ошибку, потому что её нет. Так как вы не дали постановку задачи, мы не знаем, как и ТС, что будет ошибкой.
Теперь функция защищена от ошибок, разработчик может быть абсолютно уверен, что реализация соответствует его ожиданиям.
defineElementWidth('Не число');
defineElementWidth(100); // ✅
Тут такая же фигня. О каких ошибках идёт речь? Вы поменяли один тип на другой. Зачем? У вас задача поменялась? Вроде ничего не менялось. Тогда как у вас строка превратилась в число? Вы сами ниже покажете, что это не обязательно. И по логике вам нужно было задавать не number
, а `${number}
`.
Единственное, что поменялось, вы стали явно контролировать тип возвращаемого значения и тип строки.
Так какая же изначально была задача?
Добавим поддержку значений в
px
и в%
, как во Vuetify:
Я щёлкнул по ссылке выше, и там просто string | number
. На самом деле это неважно, просто не очень выразительно. Вы уже второй раз ссылаетесь, а по ссылке этого нет. Если я не нашёл, тогда прошу прощения.
И вот мы подошли к выводу:
Вывод
TypeScript работает эффективнее, если мы сначала определяем типы, а затем реализуем логику. Это позволяет получать подсказки от IDE прямо во время написания функции.
Если посмотреть на всё выше, то возникает вопрос: "О чём это всё"? О написании типов? О том, какой тип куда и зачем нужно вставлять? Почему решили отказаться от инферинга возвращаемого значения? Тупо из-за ошибки?
Вот это вот всё было зачем:
type Unit = px | '%';
type Width = number | ${number} | ${number}${Unit};
type Style = { width: ${number}${Unit} };
function defineElementWidth(value: Width): Style {
if (typeof value === 'string') {
const match = value.match(/^(\d+)([a-zA-Z%]*)$/)?.slice(1).filter(Boolean) ?? [];
const [width = '0', unit = 'px'] = match;
return {
width: ${parseFloat(width)}${unit as Unit},
};
}
return {
width: ${value}px,
};
}
Используя утверждение типа, снова вернулись к тому, с чего начали, и снова можете использовать кириллицу, и TS вам не подсветит ошибку. Так значит, дело было не в той ошибке?
Я про вот эту строку, если что: const [width = '0', unit = 'px'] = match;
В принципе, можно и от вероятности ошибки избавиться, и даже тип полностью инферить, это при желании, конечно:
function isStringNumber(value: Width): value is ${number} {
return typeof value === 'string' && /^\d+$/.test(value);
}
function defineElementWidth(width: Width) {
if (typeof width === 'number' || isStringNumber(width)) {
const unit: Unit = 'px';
return { width: ${width}${unit} };
}
return {
width,
};
}
Так имеет ли всё-таки смысл данный вывод:
TypeScript работает эффективнее, если мы сначала определяем типы, а затем реализуем логику.
В чём корень проблемы? Когда мы говорим: "Здесь должен быть такой тип" (это отсылка на книгу "Здесь должен быть текст"), что мы имеем в виду?
А имеем мы в виду именно дизайн, то есть проектирование нашего ПО. По-хорошему, мы определяем не типы, а разбираемся с дизайном нашей программы. Все эти ошибки — это ситуации, которые мы хотим учесть и которые изначально заданы в условиях задачи. Или мы пренебрегаем этим, чтобы сэкономить время или ещё что-то, то есть упрощаем дизайн и делаем программу менее устойчивой к изменениям.
В данной статье изначально очень мало внимания уделялось дизайну, то есть правильной постановке задачи, обозначению важных для бизнеса или системы ситуаций, и поэтому типы скакали туда-сюда, потерялась первоначальная идея избежать ситуации, когда разработчик мог неверно написать значение юнита и т.п.
Действительно, когда создаём программы на js и ts, по-разному относимся к проектированию. TS требует, чтобы проектированию изначально уделялось больше внимания. Типы — это именно про то, чтобы в коде дизайн был ярко выраженным. Когда вы выбираете тип, вы в первую очередь думаете о дизайне программы, а не просто о том, чтобы это всё завелось, когда банально партируете js на ts. И вот когда вы изначально ставите себе ясную задачу и объясняете себе её детали, типы начинают работать.
Мне показалось, что вы упустили суть статьи и с головой ушли в детальный анализ оптимальности решения. Статья не направлена на то чтобы показать как писать на TS идеально.
Статья стремится показать как можно путем использования особенностей TS получать помощь в написании и проектировании кода со стороны IDE отдав ей часть мыслительной нагрузки.
В конце вы сами выводите эту мысль
Действительно, когда создаём программы на js и ts, по-разному относимся к проектированию. TS требует, чтобы проектированию изначально уделялось больше внимания. Типы — это именно про то, чтобы в коде дизайн был ярко выраженным. Когда вы выбираете тип, вы в первую очередь думаете о дизайне программы, а не просто о том, чтобы это всё завелось, когда банально партируете js на ts. И вот когда вы изначально ставите себе ясную задачу и объясняете себе её детали, типы начинают работать.
Мне даже захотелось ее скопировать в выводы, очень хорошо отражает то что хотелось донести 😉
Слушайте, не знаю. Мне показалось, что изначальная мысль: "Как заставить TS работать на вас" - недостаточно тесно связана со следующими вещами:
Нужно проектировать больше
Нужно знать и учитывать о js больше
Нужно знать и учитывать особенностей о ts больше
Потому что если целенаправленно на все это не обращать внимания, то может получится как в вашем примере. У вас теряется стройность повествования в том смысле, который описал выше потому, что на разных этапах реализуются разные задачи.
И основное, ваш пример можно разделить на две части:
Создание дизайна типов для сигнатуры функции
Написание кода самой функции
И свои трудности будут на каждом этапе. И если с дизайном типов функции создается ощущение, что вы программируете на ts, то вот с написанием кода тела функции больше похоже, что вы с ним боретесь.
У вас параметр имеет тип: `number
| ${
number
} | ${
number
}${
Unit
}
`
Как вы это учли при написании реализации?
Вы использовали: value.match(/^(\d+)([a-zA-Z%]*)$/)
Хотя, если посмотреть на тип там нет [a-zA-Z].
И выше вы сами написали, что для вас может быть проблемой использование незапланированных unit, но в реализации использовали `const
[width = '0', unit = 'px'] = match;
С последующим утверждением типа.
Поэтому я и заметил, что с одной стороны, суть у нас, что TS должен помогать, а с другой при написании кода функции вы как будто не смотрели на типа входного параметра. А просто подгоняли выходное значение под заявленный тип выходного значения.
Кстати, это нормальный подход, когда тело трудно и долго подробно типизировать. Просто для учебного примера можно было именно пойти на поводу у TS и сигнатуру типизировать и код типизировать, т.к. что бы было явно видно, что вы все это заставляете работать для вас.
Разумеется, все это не обязательно, но, мне показалось, что от подобного статься бы точно выиграла.
Мне показалось, что изначальная мысль: "Как заставить TS работать на вас" - недостаточно тесно связана со следующими вещами:
Нужно проектировать больше
Нужно знать и учитывать о js больше
Нужно знать и учитывать особенностей о ts больше
Это тема для другой статьи 😉
Как вы это учли при написании реализации?
Вы использовали: value.match(/^(\d+)([a-zA-Z%]*)$/)Хотя, если посмотреть на тип там нет [a-zA-Z].
Очень просто - никак.
Так как это не является существенной проблемой, ввиду строгих типов в любом случае не получится пропихнуть туда что-то нелегальное (если сильно не стараться, конечно 🤫).
Но соглашусь, такая не очень-то строгая регулярка может бросаться в глаза в таком контексте, поэтому поменял ее на более строгую. Лишний уровень безопасности тут не повредит.
Благодарю за обратную связь.
Это тема для другой статьи 😉
Как сказать.
Очень просто - никак.
Так как это не является существенной проблемой, ввиду строгих типов в любом случае не получится пропихнуть туда что-то нелегальное (если сильно не стараться, конечно 🤫).
Так суть же не в этом. Вы сделали целью донести идею, что нужно при работе с TS: "Обратите внимание, что мы сначала разобрались с типами, а потом шаг за шагом реализовали каждый из них получая подсказки на каждом шаге. "
И вот вы пишете: `const
match = value.match(/^(\d+)(px|%)?$/) ?? [];`
Судя по вашим типам, у вас всегда будет матч, т.к. никаких пустых строк вы не объявляли. Поэтому и ?? []
не нужно, и = '0'
тоже не нужно. Достаточно было добавить ! после match. А вы именно работали с типом string хотя сначала разобрались с типами и вывели Width. Кстати, зачем нужен parseFloat
тоже непонятно.
Я не хочу сказать, что все это существенные проблемы. Я подвожу к мысли, что с целью статьи это плохо вяжется. При том, что именно конкретный пример можно разложить по полочкам.
Локализация ослабления типа внутри функции - это обычное явление. Но используют его для упрощения дизайна типов при реализации, когда затратно и в целом нецелесообразно тратить ресурсы на подробную типизацию. Иными словами, когда реализация слишком сложная. Но у нас выразительный пример, который как раз можно сделать без этого.
Я даже не говорю, что обязательно нужен тайпгуард. Даже без него, у вас вон сколько всего можно удалить просто если посмотреть на Width
Вы зачем-то продолжаете погружаться в детали реализации и попытки что-то оптимизировать в учебном примере.
Можно писать по разному, можно типизировать с разной степенью строгости, но статья, повторюсь, не про то как правильно типизировать, а про то как облегчить себе задачу за счет превентивной проверке типов.
А уж к какому коду в итоге вас приведет такой подход зависит от ваших предпочтений и задач.
Вы можете попробовать удалить какие-то фрагменты и посмотреть к каким результатам это приведет.
Я же не настаиваю. Я постарался раскрыть свою точку зрения.
Учебные примеры на то и учебные, что на них учишься. Вы сделали акцент на одном. Я, как мне кажется, расширил и добавил смысла.
Я же не просто как то там оптимизировал, а объяснил как все это вяжется со смыслом и целями вашей статьи.
А там, конечно, каждый сам решает имеет все это смысл для него или нет.
... duble
Как заставить TS работать на вас