Збір інформації про мережеві сервіси
Однією із фаз роботи хакера є збір інформації. Пропоную більш поглиблено подивитись на один з інструментів, який автоматизовує її збір про мережеві сервіси - nmap.
Що за nmap?
Nmap (Network MAPper) - це open source інструмент, який значно облегшує досліджування мереж. За його допомогою можна не лише подивитись які порти відкриті на машинці, а й багато другої корисної інформації: яка операційна система, який сервіс, чи є вразливості.
А ще, він досить ефективний, завдяки тому що він працює з "сирими" IP пакетами. Це дозволяє йому робити сканування по різних векторах: TCP SYN, TCP Connect, UDP Scan та багато інших. Коротше — це класика зі світу "дізнатись про мережеві сервіси та порти".
Що за вектори сканування?
Коли ми вибираємо вектор сканування, ми говоримо nmap які пакети варто використовувати при аналізі цілі.
Якщо нам цікаво перевірити, чи є порт 4444 активним TCP портом, то беремо TCP Connect, який тягне за собою весь процес зʼєднання з ним, що вартує нам часу. Якщо хочемо швидко перевірити всі порти від 1 до 65К, то можемо обрати TCP SYN, який лише відправляє SYN пакет і перевіряє чи була відповідь.
Знаючи про які пакети йде мова, ми можемо обʼєктивно обрати потрібний нам вектор. Наприклад, щоб просканувати всі відкриті TCP порти на цілі за вектором TCP SYN, достатньо викликати nmap з -sS (Scan SYN) та -p- (всі порти):
sudo nmap <TARGET> -p- -sS
Результатом буде список портів, які відреагували на наші запити й з якими є сенс працювати далі.
Аналіз сервісів та їх версій
Nmap також має достатньо велику базу даних відбитків різних сервісів. Це дозволяє йому ідентифікувати який сервіс працює на порту. Сам процес ідентифікації називається probing або fingerprinting.
Для того, щоб ввімкнути цей режим, достатньо передати прапорець -sV (Scan Version). Після цього, nmap починає аналізувати інформацію, яку він здатний зібрати з сервісу. Наприклад, інформація з банера, який ви бачите на SSH сервісах, має достатньо інформації про те, щоб сказати що за операційна система і що за SSH сервіс працює:
sudo nmap <TARGET> -p 22 -sV
Результатом цієї команди буде інформація про те, що працює на 22 порту та яку версію має. Звісно ж, якщо в нього є на цей сервіс відбиток.
Скрипти на Lua?
Так, скрипти на Lua! В nmap є можливість писати свої скрипти на Lua, в нього є свій Scripting Engine. Більш того, в нього багато вбудованих скриптів, які можна викликати не вдаючись до написання своїх.
Що це за скрипти та для чого вони — велике питання, яке ми не будемо тут розкривати. Просто знайте, що їх багацько, вони поділені на категорії.
До прикладу, якщо ви знаєте, що на порту 80 працює HTTP сервер Apache httpd, то за допомогою скриптів із категорії http ми можемо дізнатись більше інформації. Для цього, використовується прапорець --script та * (wildcard для запуску всіх скриптів із цієї категорії):
sudo nmap <TARGET> -p 80 --script=http*
Результатом цієї команди буде багато цікавих даних (якщо вам пощастить, в залежності від сервісу та інших факторів, але в конкретно цьому прикладі вона не досить цікава 😅).
В нього є також і стандартний набір скриптів по замовчуванню, який можна викликати за допомогою -sC (те саме що й --script=default):
sudo nmap <TARGET> -p 80 -sC
Що за операційна система?
Так, в nmap це також є. Ви можете за допомогою прапорця -O попросити nmap проаналізувати яка операційна система та якої версії крутиться на сервері:
sudo nmap <TARGET> -O
Результатом буде список вірогідностей тої чи іншої операційної системи, яка встановлена на сервері.
А тепер все разом!
На цей випадок в nmap також є прапорець - -A. Якщо ви хочете провести повний аналіз порту, то ви можете використовувати цей прапорець і він зробить все те про що ми тут поговорили:
sudo nmap <TARGET> -p 80 -A
Багато іншого!
Я вже не влізаю, тому скажу що це не все, що вміє nmap. З його допомогою можна обходити фаєрволи, IDS (Intrusion Detection System) та IPS (Intrusion Prevention System) системи та інше. Про це все можна почитати на його сайті.
Penetration Testing Lifecycle
У кожного із нас є життєві цикли, за якими ми працюємо. У розробників це цикл "друкуй код — напиши до нього тест — віддай на тестування — повтори". У архітекторів це щось типу "вивчи проблему — обери стек для її вирішення — зрозумій, що стек вибраний невірно — повтори" (жартую).
Тобто, у кожної спеціалізації існує певний набір кроків, за якими робиться та чи інша справа. І якщо говорити про кібербезпеку, то у них також існує свій цикл, по якому вони працюють.
DISCLAIMER: як би мені не хотілось відразу почати писати про кібератаки, я вважаю що потрібно для початку зрозуміти взагалі їх формат праці. А уже потім, можна й про кібератаки та яких результатів можна досягти з їх допомогою.
Information Gathering
Це самий початок взагалі у кожного хакера, який тестує систему на злом. В цій фазі не відбувається нічого цікавого (в плані того, що зазвичай показують в фільмах). Все що відбувається — це збір інформації про ціль.
Які сервіси працюють? Чи є там FTP сервер? На якій CMS працює їх блог? І купа інших питань такого формату.
Відповіді отримуються відносно легко. Для більшої частини існують автоматичні скрипти, які це роблять за вас. Вам лише потрібно проаналізувати результат роботи цих скриптів, зробити певні висновки й записати в нотатник.
Threat Modelling
Маючи записи в нотатнику про систему, ми вже можемо починати робити припущення про вектори атаки. Чому я кажу "припущення"? Тому що, до прикладу, робочий FTP сервер ще не означає, що його можна буде зламати. Але ми можемо припустити, що цей сервер вразливий і на основі цього побудувати вектор атаки.
Як тільки у нас з'являються можливі вектори атаки, було б непогано їх перевірити. А саме, відповісти на питання, чи залишаться вони припущеннями чи підтвердяться і ми зможемо пройти далі.
Vulnerability Analysis
На цьому кроці, відбувається детальний аналіз можливої вразливості. Для перевірки правдивості наших припущень існують так звані PoC (Proof of Concept) скрипти. Зазвичай вони не агресивні й не ставлять за ціль зламати систему (DoS) чи видалити всі дані. Вони лише перевіряють, чи вразливість дійсно існує.
До прикладу, PoC для перевірки RCE (Remote Code Execution) базується на викликах команд id, whoami або cat /etc/passwd. Якщо ви отримали результат — вразливість підтверджена.
Exploitation
Маючи підтверджену вразливість, її можна почати використовувати. Не відходячи далеко від прикладу з SMB сервером, ми отримуємо доступ до операційної системи без аутентифікації себе, як користувача (посилаюсь на EternalBlue вразливість).
А якщо ми можемо виконувати наші команди на цілі, то ми можемо й отримати дані для подальшої ескалації. До прикладу, прочитати приватні ключі користувачів для підʼєднання до SSH від їх імені.
Post Exploitation
Як тільки ми успішно експлуатували певну вразливість і отримали доступ до користувача, ми, скоріш за все, опинимось користувачем з обмеженими привілеями в системі.
Але для того, щоб отримати повний доступ до системи, нам потрібен root. В цій фазі саме це і відбувається. Шукаються шляхи ескалації привілеїв від імені того користувача, який в нас уже є.
У випадку успіху, ми отримуємо доступ від імені рута і, якщо нам це потрібно, ми можемо продовжувати Post Exploitation від імені рута і далі, до тих пір, поки ми не отримаємо те що нам потрібно.
Reporting
Остання фаза із циклу — звітність. Ми всі люди виховані, просто так ніхто нікого не атакує і не має цього робити. Якщо пентестери почали когось атакувати, то лише через legal consent тої сторони, яка дала на це згоду.
А ви, як професіонал, в кінці даєте їм звітність про їх вразливості, щоб вони могли їх закрити й таким чином вберегти себе від Black Hat хакерів. Робота виконана — переходимо знову на пункт 1.
Епілог
Пишіть в коментарях, що вам було б цікаво дізнатись про сферу кібербезпеки. Більше про конкретні вразливості, може історії зламів? Чи, наприклад, якісь практичні сценарії? От нещодавно дізнався про історію, як через кондиціонер викрали декілька сотень мільярдів доларів. Думаю, я про неї якось розкажу.
Привет! Это небольшой пост, призванный осветить происходящее в Украине 🇺🇦
24 февраля, рано утром, Россия открыла огонь по нескольким мирным городам Украины. Я был вынужден покидать свой дом под звуки сирен авиационной тревоги. Сегодня 27 февраля, полномасштабная война всё ещё идёт полным ходом. Россия нарушает все возможные конвенции, открывает огонь по мирным жителям, расстреливает мирных граждан в спины, не дает возможности организовать "зеленый коридор".
Поэтому, сообщаю следующее. Это мой последний пост на этом канале на русском языке. Если вы не согласны с политикой Путина - выходите на протесты. Если вы не согласны с моей политикой - отписывайтесь от этого канала. Если вы хотите высказаться о "я же не нападаю, это всё власть" - даже не пытайтесь, я буду игнорировать такие комментарии.
UPD: вимкнув коментарі, тому що русні багацько
UPD2: видавил групу обговорень, так як вони і туди знайшли шлях
Алгебраические типы данных
В прошлый раз я заикнулся об алгебраических структурах. В комментариях выяснилось, что есть небольшая путаница между алгебраическими структурами и алгебраическими типами данных. Поэтому давайте попробуем разобраться со вторым термином.
Что у нас по определению?
Как оказалось, сложных определений здесь нет. Алгебраические типы данных - это типы, собранные из других типов данных. Всё. Буквально. Это конец определения.
Я почти уверен, что вы все используете алгебраические типы данных, но не подозреваете об этом. Или подозреваете, если вы знакомы с этим, конечно же. И, да, давайте дальше будем использовать сокращенную форму Algebraic Data Type (ADT), так меньше букв.
Так вот, любой тип, который состоит из других типов - ADT. Это объект? Массив? Кортеж? Все они типы данных, которые содержат в себе другие типы. Следовательно, их можно назвать ADT. И можно было бы закончить, но нет... Рассмотрим две категории ADT.
Product Types
Первая из них - Product Types. Это типы, которые позволяют хранить в одной структуре разные типы. Например, возьмем MyStruct(bool). Это тип, который хранит в себе другой тип - булевый. Булевых значений может быть два: true или false.
Сколько можно создать "корректных" структур MyStruct? Всего два: один с MyStruct(true) и второй с MyStruct(false).
А если мы добавим ещё один тип? В итоге у нас получится MyStruct(bool, bool). Структура уже держит два булевых типа внутри, а вариантов создания "корректной" структуры у нас становится 4. Это (true, true), (true, false), (false, true) и (false, false).
Поэтому они и называются product types. Чтобы посчитать количество возможных вариантов структуры, нам нужно умножать варианты между собой. В нашем случае 2 * 2. Получаем 4 возможных варианта.
Sum Types
Есть ещё вторая категория - Sum Types. В отличие от Product Types, их задача хранить какой-то один тип из заранее известных. Да, те самые enum-ы или tagged unions - это sum types. Вы указываете какие варианты могут быть и выбирается только одно, не все сразу.
Например, мы можем сказать, что у нас есть 4 стороны света: north, east, west, south. И у нас есть структура Side(direction). В direction мы можем указать только что-то одно из этого. А значит, вариантов всего может быть 4.
Но что произойдёт, если мы позволим в direction указывать это ещё и в виде чисел от 0 до 3? В таком случае, direction будет либо строкой, либо числом. Но так как это всё ещё один вариант, мы не можем указать разные одновременно, то и количество вариантов остается 4 (north, east, west, south) + 4 (0, 1, 2, 3).
Поэтому эти типы и называют sum types, потому что они суммируют варианты, чтобы узнать количество возможных.
И к чему это всё?
Интересный момент здесь в том, что компилятор, зная о возможных вариантах, может за вас проверить все возможные случаи и предотвратить ошибку ещё на этапе компиляции.
Мы воспользовались sum types и указали, что у нас может быть 3 состояния в системе? Компилятор посчитал возможные варианты, проверил что все три состояния обрабатываются вашим кодом.
Мы воспользовались product types и указали, что мы можем одновременно указать стороны света и текущую температуру? Компилятор проверит что стороны света это строки из четырех вариантов, указанными нами, и что текущая температура это число.
Подытожим
Алгебраические типы данных (ADT) могут быть двух типов: product types и sum types. Product types используются тогда, когда вы хотите выразить возможность указывать данные одновременно с другими данными в рамках одной структуры. Sum types используются, когда вы хотите указывать данные из определенного списка и только что-то одно.
Disclaimer
Друзья из Framework Days делают конференцию по React 26 февраля. Планируется, в том числе, и offline формат (наличие теста или вакцинации). Так что забегайте на сайт, смотрите программу, есть толковые спикеры. Там ещё могут моменты поменяться, но не думаю что кардинально что-то. Постараюсь и я попасть на offline формат, может увидемся.
TypeScript Features to Avoid
Disclaimer
Всем привет! Меня не было на канале почти 2 месяца. Но оно того стоило, скажу я вам. Я ушел в отпуск перед Новым годом, не прикасался ни к каким ноутбукам, пет проектов никаких не делал. В итоге я очень хорошо отдохнул и вернул себе эмоциональное спокойствие (наконец-то!) 🤟
У меня постепенно получается вернуться в привычный режим работы, так что скоро я начну писать на канале дальше. Пока меня не было, накопилось очень много интересных блог постов от других авторов, которыми я бы хотел поделиться и обсудить. Ну а теперь, к посту!
Пролог к посту
18 января 2022 вышел пост с названием "TypeScript Features to Avoid". В нём автор упоминает некоторые возможности языка которые, по его мнению, стоит избегать. Давайте по ним и пройдёмся, я добавлю своё мнение, а вы пишите в комментариях, что вы думаете.
Избегайте enum-ов
Есть лагеря которые, в целом, против использования enum-ов. Лично нам они помогают моделировать некоторые ситуации и очень хорошо их моделировать. Но у них и правда есть пара нюансов.
Автор в статье упоминает, что использование enum-ов приводит к дополнительному коду в конечном JavaScript коде, который и реализует сам enum. Это дополнительный объект со значениями, которого не было в изначальном коде (кроме типа).
И это действительно может стать проблемой, если вы используете другие инструменты, помимо TypeScript, например Babel или Webpack. Эти инструменты изначально делались для JavaScript и если по какой-то причине, они не поймут что за enum-ы и как с ними работать, то они просто сделают type stripping, что сломает код в runtime. Если же вы используете только TypeScript, то вряд ли с этим могут быть проблемы.
Лично от меня ещё одно замечание, что у enum-ов неявно присваиваются значения. Пока вы не трогаете порядок его элементов, всё хорошо. Но как только вы начнёте перемешивать или сортировать ваш enum, TypeScript неявно присвоит порядковые номера и вследствие этого, в runtime будут уже не те числа, которые вы ожидали. Об этом можно подробнее почитать вот в этом ESLint правиле.
Избегайте namespaces
Полностью поддерживаю и даже добавить нечего. Мало того что они, как и enum-ы, добавляют "своего" JavaScript-а в runtime. Так это ещё и артефакт прошлого, когда не было EcmaScript Modules и каждый писал свою систему модулей.
На сегодняшний день, namespaces считается артефактом. Он также не рекомендуется использоваться и самой командой TypeScript. Используйте вместо них обычный ESM и не делайте никому жизнь хуже.
Избегайте декораторов
И здесь тоже полностью поддерживаю автора. Хоть декораторы и открывают новые возможности для тех кто любит мета программирование всякое, они всё ещё далеки от попадания в спецификацию и много чего может в них поменяться.
Мы уже попадали в некоторые ситуации, когда декораторы были бы интересным решением, и мы хотели их начать использовать. Но каждый раз нас останавливало то, что это не установившийся стандарт. А поддерживать (потенциально) меняющийся API не сильно хочется.
Избегайте private модификаторов
Ну тут уже вкусовщина чистой воды. Автор говорит о том, что вместо использования private, лучше используйте приватные свойства самого JavaScript (который через # обозначается).
Лично для меня, каких-то больших аргументов "за" я не увидел. А ломать стилистику кода, когда у тебя public используется, а рядом # - ну... глаз дёргаться будет. Поэтому мы используем public, private, protected и никаких проблем с этим не имеем.
Эпилог
Хотелось бы подытожить следующее:
- Если у вас только TypeScript, используйте enum-ы, ничего плохого в этом нет. И лишний раз удостоверьтесь, что ничего не ломается, если есть другие инструменты.
- Избегайте namespace-ов и декораторов - полностью поддерживаю.
- Не навязывайте вкусовщину (кроме своих коллег, конечно же 🙃).
Пишите в комментариях, что вы думаете об этих возможностях языка. Нужны они, не нужны, полезные, не полезные, в общем - набрасывайте - подискутируем 🥃
Type Challenges: PickByType
Проблема
Из входного параметра T выберите те свойства, которые присваиваемые к U. Например:
type OnlyBoolean = PickByType<{
name: string
count: number
isReadonly: boolean
isEnable: boolean
}, boolean> // { isReadonly: boolean; isEnable: boolean; }
Решение
В этой проблеме нам нужно перебрать все ключи объекта. В момент перебора, нам необходимо отфильтровать и оставить только те ключи, которые присваиваемые к U. Очевидно, что начать нам нужно с сопоставляющих типов.
Так что давайте начнём с того, что сделаем просто копию входящего объекта:
type PickByType<T, U> = { [P in keyof T]: T[P] }
Сначала, мы получили все ключи из T и применили к ним итерацию. На каждой итерации, TypeScript возьмет ключ и присвоит его к параметру P. Имея этот ключ, мы можем получить нужный нам тип с использованием конструкции T[P].
Теперь, применяя фильтр к этой итерации, мы сможем оставить только нужные нам ключи. Когда я говорю "фильтр", я подразумеваю переопределение ключа в этой ситуации. Мы можем переопределить ключ на каждой итерации и оставить только интересующие нас:
type PickByType<T, U> = { [P in keyof T as T[P] extends U ? never : never]: T[P] }
Обратите внимание на ключевое слово as. Это оператор, который заменит наш тип P на тип указанный с помощью as. И нас ничего не останавливает от того, чтобы написать условный тип после as. С помощью этого условного типа мы и проверим наше условие.
В случае, если тип значения из объекта присваиваемый к типу, который указан в U - оставляем ключ без изменений. Но, если тип оказывается не присваиваемый, то в таком случае мы должны его игнорировать - возвращаем never:
type PickByType<T, U> = { [P in keyof T as T[P] extends U ? P : never]: T[P] }
Таким образом, мы реализовали тип, который может фильтровать ключи объектов по их типам.
Что почитать
- Сопоставляющие типы
- Переопределение ключей с помощью as
- Условные типы
- keyof и типы поиска
Web-версия
English - https://ghaiklor.github.io/type-challenges-solutions/en/medium-pickbytype.html
Русский - https://ghaiklor.github.io/type-challenges-solutions/ru/medium-pickbytype.html
Что почитать
- Условные типы
- Выведение в условных типах
- Строчные тип литералы
Web-версия
English - https://ghaiklor.github.io/type-challenges-solutions/en/medium-percentage-parser.html
Русский - https://ghaiklor.github.io/type-challenges-solutions/ru/medium-percentage-parser.html
ChowJS: AOT JavaScript Engine
Попалась мне статья о ChowJS, в которой рассказывается опыт о создании AOT компилятора для JavaScript. Довольно интересно, но не вдаваясь в технические детали реализации, рассказывают о подходах, которые они применили.
Этот компилятор в основном базируется на движке QuickJS (это один из самых маленьких движков для JavaScript, которые легко встраивать в свои приложения). Они же его и используют для того, чтобы интерпретировать JavaScript.
Загвоздка в том, что они его интерпретируют только один раз, при самой компиляции. Чаще всего, после запуска вашего JavaScript кода, прототипы не меняются, глобальные переменные не меняются и так далее. Основываясь на этом предположении, ChowJS один раз интерпретирует JavaScript с помощью QuickJS и уже после, использует полученный контекст выполнения для дальнейшей компиляции.
Это можно выразить в таком простеньком алгоритме:
- Запускаем JavaScript в QuickJS движке для его изначального запуска и инициализации
- Для каждого метода получаем байт-код и трансформируем его во внутреннее представление ChowJS
- Применяют оптимизирующий компилятор к этому IR (внутреннее представление)
- Получившееся представление компилируют в код на С, который в последствии и компилируют в машинный код
В целом очень интересная статья, рекомендую к прочтению. Дальше по статье автор разбирает байт-код, внутреннее представление, делится идеями о будущем этого инструмента.
Type Challenges: MinusOne
Проблема
Вы получаете на входе число (всегда положительное). Ваш тип должен вернуть то же число, только на единицу меньше. Например:
type Zero = MinusOne<1> // 0
type FiftyFour = MinusOne<55> // 54
Решение
Эта задача и вправду довольно сложная. TypeScript не может ничего предложить для работы с числами - ничего!
Поэтому, мы должны как-то найти обходной путь для реализации такой математической операции. И, поймите, что это вряд ли будет отличное решение и использовать его как пример для подражания вряд ли получится.
Я начал с того, что спросил себя. Были ли у нас случаи, когда мы работали с числовыми литералами, но без использования самих литералов. И, как оказалось, да. Мы использовали кортежи.
У нас уже были задачи, в которых мы работали с кортежами и узнавали длину этих кортежей. Помните синтаксис для этого? Это было очень просто - мы обращались к свойству length, которое и возвращало нам числовой тип литерал.
Поэтому я подумал, а что если мы создадим кортеж нужной нам длины и выведем его часть без последнего элемента. А после, возьмем длину этого выведенного кортежа.
Давайте начнём с вспомогательного типа, который будет нам создавать кортеж нужной длины. Назовём его Tuple:
type Tuple<L extends number, T extends unknown[] = []> = never;
Он принимает в качестве аргументов длину кортежа и временный аккумулятор. Этот аккумулятор будет накапливать кортеж, пока мы не получим кортеж нужной длины. Чтобы эту проверку реализовать, обратимся к свойству length и сравним его с требуемым:
type Tuple<L extends number, T extends unknown[] = []> = T[‘length’] extends L ? never : never;
Как только мы получили кортеж нужной длины - возвращаем его:
type Tuple<L extends number, T extends unknown[] = []> = T[‘length’] extends L ? T : never;
Но, если же мы не получаем кортеж нужной длины, нам нужно добавить к нему ещё один элемент. И продолжать так стоит до тех пор, пока не получим ожидаемую длину кортежа:
type Tuple<L extends number, T extends unknown[] = []> = T[‘length’] extends L ? T : Tuple<L, [...T, unknown]>;
Теперь, если мы вызовем наш тип с параметром 5, например, то мы получим кортеж длиной в 5 элементов и типа unknown. Если же мы обратимся к свойству length на этом кортеже, то мы получим числовой литерал - 5. То что и нужно было.
Как же достать литерал 4 из такого кортежа? Вывести кортеж, длина которого 5, но без последнего элемента. Другими словами, кортеж на один элемент будет короче.
type MinusOne<T extends number> = Tuple<T> extends [...infer L, unknown] ? never : never;
Используя такую конструкцию, мы получим в тип параметре L кортеж без последнего его элемента. Всё что остается сделать - это вернуть длину выведенного кортежа.
type MinusOne<T extends number> = Tuple<T> extends [...infer L, unknown] ? L[‘length’] : never;
Таким образом, мы реализовали некое подобие математической операции в системе типов. Например, вызывая наш тип с параметром 5, мы получим числовой литерал 4.
type Tuple<L extends number, T extends unknown[] = []> = T[‘length’] extends L ? T : Tuple<L, [...T, unknown]>;
type MinusOne<T extends number> = Tuple<T> extends [...infer L, unknown] ? L[‘length’] : never;
Но! Большое "но"! В последних версиях TypeScript, они добавили проверку на количество рекурсивных вызовов. Поэтому, если честно, мы не пройдём тесты, в которых используются числа больше 50. Так что это решение сложно назвать решением.
Если у вас есть идеи получше, не стесняйтесь, пишите их с объяснениями в комментариях ниже. Спасибо!
Что почитать
- Дженерики
- Индексные типы доступа
- Условные типы
- Выведение в условных типах
- Рекурсивные условные типы
- Вариативные типы
Web-версия
English - https://ghaiklor.github.io/type-challenges-solutions/en/medium-minusone.html
Русский - https://ghaiklor.github.io/type-challenges-solutions/ru/medium-minusone.html
Українська - https://github.com/ghaiklor/type-challenges-solutions#how-to-contribute
Type Challenges: ReplaceKeys
Проблема
Реализовать тип ReplaceKeys, который заменяет ключи в объединениях. Если какого-то ключа нет в элементе, просто пропускаем. Такой тип принимает три аргумента. Например:
type NodeA = {
type: 'A'
name: string
flag: number
}
type NodeB = {
type: 'B'
id: number
flag: number
}
type NodeC = {
type: 'C'
name: string
flag: number
}
type Nodes = NodeA | NodeB | NodeC
// would replace name from string to number, replace flag from number to string
type ReplacedNodes = ReplaceKeys<Nodes, 'name' | 'flag', { name: number, flag: string }>
// would replace name to never
type ReplacedNotExistKeys = ReplaceKeys<Nodes, 'name', { aa: number }>
Решение
У нас на руках объединение интерфейсов и нам нужно по ним итерироваться. В процессе итерации, нужно заменить ключи на требуемые, переданные через параметры. Дистрибутивность здесь, очевидно, сильно поможет, как и сопоставляющие типы.
Я начну с такого небольшого факта, как "сопоставляющие типы в TypeScript также дистрибутивные". То есть, если вы напишете сопоставляющий тип, который принимает объединение - поведение будет дистрибутивным. Таким образом, мы сможем итерироваться как по объединению, так и по ключам самого интерфейса одновременно. Давайте постараюсь объяснить.
Вы уже наверняка знаете, что условные типы дистрибутивные. Такое свойство языка нам очень часто помогало в проблемах ранее. Каждый раз, когда мы писали U extends any ? U[] : never, мы получали внутри правдивой ветки один элемент со всего объединения на каждой итерации. Компилятор делает это за нас.
Аналогичное свойство присутствует и в сопоставляющих типах. Мы можем написать сопоставляющий тип и, если на входе объединение, компилятор будет неявно применять дистрибутивность. Следовательно, мы будем работать с отдельным элементом из объединения.
Так что давайте начнем с простого. Мы возьмем каждый элемент из U (спасибо дистрибутивности) и на каждом элементе возьмем список ключей, вместе с его типами значений.
type ReplaceKeys<U, T, Y> = { [P in keyof U]: U[P] }
Таким образом, мы скопировали один в один всё, что пришло через параметр U. Теперь, нам нужно применить фильтр и работать только с теми ключами, которые есть в T и Y.
Для начала проверим, а есть ли текущее свойство в параметре T (в списке ключей, которые нам нужно обновить).
type ReplaceKeys<U, T, Y> = {
[P in keyof U]: P extends T ? never : never
}
Если это так, это значит что разработчик попросил нас поменять указанный ключ. И, скорее всего, он указал и тип, на который нужно произвести замену. Но, мы не можем быть уверенны в этом. Поэтому, дополнительно проверим, что это свойство присутствует в Y.
type ReplaceKeys<U, T, Y> = {
[P in keyof U]: P extends T ? P extends keyof Y ? never : never : never
}
И только в ситуации, когда оба условия правдивые, мы можем быть уверенны в необходимости замены. Берём тип из Y и производим замену.
type ReplaceKeys<U, T, Y> = {
[P in keyof U]: P extends T ? P extends keyof Y ? Y[P] : never : never
}
Однако, если окажется, что такого ключа нет в Y, но он есть в T, нам нужно вернуть never (согласно постановке задачи). Остается последний вариант, когда оба условия неправдивые. При таком варианте, мы просто возвращаем тот же тип, который был и в оригинальном интерфейсе.
type ReplaceKeys<U, T, Y> = {
[P in keyof U]: P extends T ? P extends keyof Y ? Y[P] : never : U[P]
}
Имея дистрибутивные сопоставляющие типы, мы смогли реализовать очень даже читаемое и поддерживаемое решение. Без них, мы были бы вынуждены использовать дистрибутивные условные типы с дальнейшим применением сопоставляющих внутри условного типа. Что, очевидно, не так хорошо смотрится, как это решение.
Что почитать
- Сопоставляющие типы
- Условные типы
- Индексные типы поиска
Web-версия
English - https://ghaiklor.github.io/type-challenges-solutions/en/medium-replacekeys.html
Русский - https://ghaiklor.github.io/type-challenges-solutions/ru/medium-replacekeys.html
Українська - https://github.com/ghaiklor/type-challenges-solutions#how-to-contribute
Монорепы и инструменты сборки
Всем привет! Просматривал сегодня утром беклог постов на канал и заметил такую тему как монорепы. Вспомнил, что у нас недавно ещё была и дискуссия на Framework Days. Мы обсуждали их и что вообще это, зачем, как живется с ними. Так что почему бы не поговорить о них и здесь.
Что такое монорепа?
Монорепа - это репозиторий, в котором хранятся разные компоненты системы, сервисы, приложения, библиотеки и прочее. Всё это хранится в одном репозитории Git, Mercurial, SVN и других системах контроля версий (на ваш выбор, разумеется).
Давайте на примере
Вот у вас есть одна библиотека и вы делаете для неё отдельный репозиторий. Появляется вторая библиотека и вы делаете снова для неё отдельный репозиторий. Это "классический" подход, которым уже давно пользуются.
Но, это станет монолитом, если вы не будете делать под каждую библиотеку отдельный репозиторий, а просто сольете всё в одно приложение, которое хранится в одном репозитории. В таком случае, у вас приложение монолит и репозиторий (следовательно) тоже называют монолитом.
А вот монорепой это становится тогда, когда все ваши библиотеки всё ещё имеют свои собственные билд процессы, публикуются как отдельные артефакты, они взаимозаменяемые, но при этом хранятся в одном и том же репозитории, да ещё и в соседней папке, в которой (может) работает совсем другая команда. И идеальные монорепы хороши тем, что её сборка со всеми внутренними зависимостями всегда воспроизводится в один и тот же артефакт(ы). Такой подход обычно называют "One Version". Но это другая тема на следующий пост, напишите в комментариях, было бы интересно узнать что за подход такой?
А сегодня я бы хотел рассказать об инструментах, которые позволяют нам управлять этими монорепами и не сойти с ума. Начнём с самого простого подхода.
yarn/npm/pnpm workspaces
Отличное решение для малых и средних размеров монореп. Кривая входа в такой подход очень низкая и требует от разработчика всего лишь указания в package.json где лежат все ваши пакеты и пакетный менеджер сделает всё остальное за вас.
Он слинкует локально интра-зависимости между собой, поставит из репозиториев зависимости третьих лиц и в целом подготовит вам локально рабочее место, с которым можно работать.
Их слабое место в том, что такое решение очень плохо масштабируется. Если у вас большая компания и "идеальная" монорепа, то таких пакетов может быть тысячи и тысячи. А если у вас оно будет настроено на workspaces, то даже локальный pnpm install будет занимать вечность, а pnpm run build будет собирать репозиторий часами.
Почему? Потому что у workspaces нет инкрементальной компиляции из коробки, нет фильтров, позволяющих устанавливать зависимости только для подмножества проектов, с которыми вы работаете в данный момент (другие вас попросту не интересуют). За счёт того, что workspaces ставят все зависимости в общий корень, доступный всем пакетам, дистрибутивные билды на workspaces тоже не сделаешь (нужно делать своё решение).
Поэтому если у вас небольшая команда и проблем вида "жду два часа, пока билд сделается" нет - workspaces прекрасный инструмент для этого.
Lerna
У Lerna очень длинная история и появилась она задолго до того, как появились workspaces. Она решала проблему монореп таким же образом, как это делают сейчас те же workspaces. Поэтому, как мне кажется, с их появлением, необходимость в lerna постепенно отпадает.
Но для консистентности, давайте всё же немного о ней поговорим. Кривая входа в неё где-то на том же уровне, на котором и workspaces. Вы создаёте файл lerna.json, в котором описываете где находятся ваши пакеты и lerna делает всё остальное. Проблемы и преимущества у неё такие же, как и у workspaces.
Есть возможность смотреть разницу между состояниями, какие пакеты были изменены. Можно управлять версиями, публиковать пакеты и всё это через одну точку входа, которая делает всё за вас.
Так что если говорить, опять же, о небольших репозиториях, lerna прекрасный выбор. Но, скорее всего, вам будет достаточно и workspaces.
Type Challenges: Remove Index Signature
Проблема
Реализовать RemoveIndexSignature<T>, который исключает индексную сигнатуру с объектов. Например:
type Foo = {
[key: string]: any;
foo(): void;
}
type A = RemoveIndexSignature<Foo> // expected { foo(): void }
Решение
Мы имеем дело с объектами в этом случае. Уверен, нам понадобятся сопоставляющие типы. Но, пока что, давайте разберемся в проблеме и что нам нужно сделать.
Нас попросили исключить индексные сигнатуры из объектных типов. Как эти сигнатуры выглядят? Используя оператор keyof, посмотрим, как TypeScript видит такие сигнатуры с точки зрения ключей объекта.
Например, имея тип Bar, на котором вызовем keyof, мы увидим следующую картину:
type Bar = { [key: number]: any; bar(): void; } // number | “bar”
Получается, что каждый ключ на объекте представлен как строковый тип литерал. В то же время, индексные сигнатуры представляются как общие типы, например number.
Это наводит меня на мысль, что мы можем отфильтровать и оставить только тип литералы. Но, как мы сравним или узнаем, является ли тип литералом или нет?
Воспользуемся тем, как ведут себя множества. Например, строчный литерал foo входит в множество строк, но строки не входят в множество foo. Потому что foo это множество из одного элемента и никак не покроет все строки.
"foo" extends string // true
string extends "foo" // false
Давайте это же свойство и применим в нашей проверке на литералы. Для начала, проверим случай со строками:
type TypeLiteralOnly<T> = string extends T ? never : never;
В случае, если T это string, условие выполнится с результатом true и мы вернём never. Почему? Потому что нам не нужны общие типы, нам нужны только литералы. Следовательно, мы пропускаем string. Такая же логика применима и к другому типу - number.
type TypeLiteralOnly<T> = string extends T ? never : number extends T ? never : never;
Что если T и не string и не number? Это значит что у нас сейчас тип литерал, который мы можем вернуть обратно как результат.
type TypeLiteralOnly<T> = string extends T ? never : number extends T ? never : T;
У нас на руках есть обёртка, которая возвращает нам только тип литерал и пропускает общий тип. Давайте применим это к каждому ключу объекта, используя сопоставляющие типы. Сделаем копию объекта, с которой будем работать:
type RemoveIndexSignature<T> = { [P in keyof T]: T[P] }
Во время итерации на ключах, мы можем поменять его тип, используя оператор as. Воспользуемся этим и добавим нашу обёртку:
type RemoveIndexSignature<T> = { [P in keyof T as TypeLiteralOnly<P>]: T[P] }
Таким образом, на каждой итерации, мы вызываем вспомогательный тип TypeLiteralOnly. Который, в свою очередь, возвращает переданный тип, если это литерал, и never, если индексная сигнатура.
type TypeLiteralOnly<T> = string extends T ? never : number extends T ? never : T;
type RemoveIndexSignature<T> = { [P in keyof T as TypeLiteralOnly<P>]: T[P] }
Что почитать
- Условные типы
- Сопоставляющие типы
- Подмена типа в сопоставляющих типах
Web-версия
English - https://ghaiklor.github.io/type-challenges-solutions/en/medium-remove-index-signature.html
Русский - https://ghaiklor.github.io/type-challenges-solutions/ru/medium-remove-index-signature.html
Українська - https://github.com/ghaiklor/type-challenges-solutions#how-to-contribute
Всем привет! Давайте сегодня разбавим посты о Type Challenges чем-то более практичным. Расскажу вам сегодня немного о генерации TypeScript кода и библиотеке, которая помогает с этим.
Зачем вообще может пригодиться генерация кода? Это довольно редкая и узкая штука, в большинстве случаев вы вряд ли с этим столкнетесь. Но, если столкнетесь, то сразу поймете - это оно - это генерация кода 🙃
Например, в одном из наших сценариев, мы используем генерацию кода для решения проблем "бойлерплейта". Разработчик описывает возможности нашего фреймворка, которые он хочет использовать, и описывает ключевые параметры этих возможностей (под возможностями я подразумеваю capabilites, просто так перевёл). После этого в игру вступает анализ этого дескриптора. И на его основе мы уже генерируем много TypeScript кода, который буквально реализовывает нужные capabilites для конечного пользователя.
Ещё один сценарий - трансформирование кода. Вы, наверняка, слышали о такой штуке как codemods. Их обычно пишут для более простой миграции из X в Y (например, миграция с Mocha на Jest). Такие codemods берут ваш исходный код и преобразовывают его в другую форму (например, заменяют вызов .toBe на .toStrictEqual).
Эти все сценарии о том, что они позволяют автоматизировать рутинную работу написания или изменения кода в некоторых сценариях. Все эти сценарии затрагивают работу с абстрактным синтаксическим деревом, его модификацией и принтерами для него.
И всё бы ничего, но работать напрямую с ними, конечно, интересно, но всё же не практично. Поэтому существуют библиотеки разных видов, которые скрывают от вас эту сложность. Одна из них - ts-morph.
ts-morph это библиотека, которая предоставляет обёртку поверх TypeScript Compiler API. TypeScript Compiler API предоставляет вам строительные блоки из которых вы можете строить свои абстрактные синтаксические деревья, манипулировать существующими и так далее. Но они относительно базовые и на больших проектах с ними не совсем удобно работать. Поэтому есть ts-morph, который даёт вам множество вспомогательных функций по работе с этим API.
Возьму самый примитивный пример. Например, вы захотели автоматизировать переименование вашего enum. С помощью ts-morph это будет очень просто. Сначала, мы создадим проект с использованием ts-morph API и после этого просто вызовем rename метод:
const project = new Project({ tsConfigFilePath: "path/to/tsconfig.json" });
const myEnum = project.getSourceFile('MyFile.ts').getEnum("MyEnum");
myEnum.rename("NewEnum");
И похожих API там довольно много, которые позволяют манипулировать деревом проще, чем даёт это TypeScript Compiler API. Так что советую вам посмотреть на эту библиотеку, если у вас в работе есть сценарии, где нужно модифицировать TypeScript код. Мы её используем у себя и для некоторых это "вечный молоток" 🔨
Пишите в комментариях, приходится ли вам манипулировать кодом? В каких сценариях? Может слышали об интересных codemod-ах?
https://github.com/dsherret/ts-morph
Type Challenges: AnyOf
Проблема
Реализовать функцию any из языка Python на уровне типов. Тип принимает кортеж и возвращает true, если любой из элементов кортежа true. Если же кортеж пустой, то возвращаем false. Например:
type Sample1 = AnyOf<[1, "", false, [], {}]>; // expected to be true
type Sample2 = AnyOf<[0, "", false, [], {}]>; // expected to be false
Решение
Моей первой идеей было использовать дистрибутивные условные типы.
Мы можем использовать синтаксис T[number] для того, чтобы взять объединение всех элементов из кортежа. Имея объединение элементов, переберём каждый из них и на каждой итерации будем возвращать false или true в зависимости от типа элемента.
В случае, если все итерации вернут false, мы получим тип false. Но, если у нас будет хоть один true, то мы получим boolean. Потому что false | true = boolean. Проверяя что у нас получилось в результате, false или boolean, мы можем понять, есть ли true в объединении.
Но, как оказалось, реализация такого подхода выглядит не очень хорошо. Оцените сами:
type AnyOf<T extends readonly any[], I = T[number]> = (
I extends any ?
I extends Falsy ?
false :
true :
never
) extends false ? false : true;
Поэтому я задумался, а можем ли мы решить это более простым способом? Чтобы это хоть как-то можно было понять, что происходит, и не потеряться в будущем. Оказывается, мы можем!
Давайте вспоминать о выведении типов в кортежах и о вариативных типах. Помните, мы их использовали в решениях таких задач как Last или Pop и им подобные.
Начнём с выведения одного элемента из кортежа и всего остального, что есть в хвосте:
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? never
: never;
Как мы можем проверить, что наш H это false? Для начала, давайте создадим новый тип, в котором укажем какие элементы мы считаем за false:
type Falsy = 0 | '' | false | [] | { [P in any]: never }
Имея такой тип, мы можем использовать условный тип для проверки, входит ли H в их число:
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? H extends Falsy
? never
: never
: never;
Что нам делать, если тип оказался false? Для нашей задачи это означает, что true ещё не найден и мы продолжим его искать. Реализуем это через рекурсивный вызов типа с хвостом из кортежа в качестве аргумента.
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? H extends Falsy
? AnyOf<T>
: never
: never;
Подходим всё ближе к решению проблемы. Что если элемент H это не false? Значит это true, мы нашли его! Раз мы нашли true в кортеже, то продолжать нет смысла и мы просто возвращаем true.
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? H extends Falsy
? AnyOf<T>
: true
: never;
Последнее состояние, которое у нас всё ещё не обработано - пустой кортеж. В случае с пустым кортежем, наше выведение не сработает. В таком случае, как и указано в условии задачи, мы возвращаем false.
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? H extends Falsy
? AnyOf<T>
: true
: false;
Таким образом мы реализовали функцию any из Python на уровне системы типов у TypeScript. Вот полное решение проблемы:
type Falsy = 0 | '' | false | [] | { [P in any]: never }
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer T]
? H extends Falsy
? AnyOf<T>
: true
: false;
Что почитать
- Типы объединений
- Условные типы
- Выведение в условных типах
- Дженерики
- Вариативные типы
Веб-версия
English - https://ghaiklor.github.io/type-challenges-solutions/en/medium-anyof.html
Русский - https://ghaiklor.github.io/type-challenges-solutions/ru/medium-anyof.html
Українська - https://ghaiklor.github.io/type-challenges-solutions/uk/medium-anyof.html
Всем привет! Сегодня решил поднять очень сложную тему. Сложная она по той причине, что объективно "правильного" ответа на неё нет. И можно даже поднять споры на тему того, что же считать "правильным" в этой теме. Поэтому, пишу жирными буквами, это просто моё личное мнение.
Где статическая типизация полезная?
На проектах средней и большой сложности, где объем работ превышает объем, который может делатся одним человеком. Над проектом могут работать несколько распределенных команд и так как проект большой с большим количеством кода, то вы не в силах знать все тонкости проекта и все проблемы, которые решаются.
Поэтому, наличие инструмента, с помощью которого каждый участник разработки может оставить "полезную информацию" о своем решении, очень сильно упрощает жизнь его коллегам. Коллеги не лезут в "черную коробочку", они только знают о том какие у неё есть входы и какие выходы. А инструмент (компилятор) проверяет что все ваши ожидания всех ваших коллег соответствуют.
Более того, компиляторы обладают информацией о том, как ваш код будет выполняться (control flow analysis, data flow analysis). Он может проложить возможные пути и вывести из этого возможные сценарии, которые вы можете получить в конкретном месте. Это помогает не забывать обрабатывать разные ситуации или ошибки, которые, по вашему мнению, не могли бы случиться.
Если попытаться подытожить, то я беру языки программирования со статической типизацией, когда:
- я работаю в команде
- хочу предостеречь себя от ошибочных сценариев, которые я забыл обработать
- иметь всяческую поддержку от редакторов и инструментов, которые используют статическую информацию для подсказок и проверок разного рода
Где статическая типизация (не)полезная?
Это, конечно, лично выбор каждого, но если вы работаете один и проект у вас небольшой, то возможно и смысла тянуть статическую типизацию нет. Особенно, если это проект малой сложности и сильно много изобретать не придется.
Вторая сторона медали статической типизации заключается в том, что скорость разработки понижается. Скажем, вы хотели себе просто сделать CLI с двумя командами, которые бы запускали другие процессы. И вместо того, чтобы на том же JavaScript или Bash за 5 минут реализовать его, вы бы потратили несколько часов на объяснение компилятору, а что вы вообще хотите сделать.
Поэтому, если проект у вас небольшой, вы работаете один, то я не вижу и причин "заставлять" использовать язык со статической типизацией. Я иногда так и поступаю. У меня есть несколько проектов, которые были сделаны без использования каких-либо языков со статической типизацией и они прекрасно себя чувствуют.
Так что я бы осознанно отказался от использования языка со статической типизацией, когда:
- мне нужно быстро сделать какой-то минимальный продукт и нет необходимости в дальнейшей его поддержке
- проблема, которую я решаю, можно решить автоматизацией с использованием других средств (скрипты)
- компоненты, которые планируются реализоваться, являются лишь некими "расширениями" основного функционала (ядра)
А вы как думаете? Пишите в комментариях, когда бы вы использовали языки программирования со статической типизацией, а когда осознанно бы отказались от них?
The curse of strong typing
Всім привіт! Сьогодні не буде лонгріду на 4096 символів, які зазвичай телеграм дозволяє відправити одним повідомленням. Але буде посилання на лонгрід від другого розробника - fasterthanlime 😁
У своєму пості він розповідає про проблеми повʼязані з мовами програмування, в яких є сильна система типів. Це зроблено в дуже цікавому форматі й читати цей пост саме задоволення.
Якщо ви підписані давно, то знаєте, що я почав цей канал вести саме про типи в TypeScript. І, можливо, памʼятаєте моє ставлення до людей які кричать "типи не потрібні" або "типи потрібні всюди". Я ж все-таки дотримуюсь ідеї — всьому свій інструмент, головне знати сильні та слабі сторони.
І, на мою думку, саме в цьому пості можна побачити альтернативне бачення на мови с сильними системами типів та які незручності ви можете від них отримати. Тому, бажаю приємного читання, вам має сподобатись.
https://fasterthanli.me/articles/the-curse-of-strong-typing
P.S. В наступних постах я буду потрохи писати про різні тули, якими користуються пентестери для того, щоб автоматизовувати процес хакінгу. Так що буде і про nmap, і про sqlmap, і про ZAP та багато іншого, коротше має бути цікаво.
Всім привіт! Сьогодні вже 18 травня, але війна все ще в країні та дуже важко просто ось так взяти й повернутись до звичного життя, яке колись в нас було.
Хтось поїхав в другі країни. Хтось переїхав тимчасово в західні області України. Хтось почав цілими днями волонтерити. Хтось цілими днями кошмарив пропаганду, їх цифрові державні послуги та інше. А хтось давав відсіч оркам з росії на передовій. Життя багатьох дуже швидко змінилось і навряд чи вже повернеться до того як було. А в когось його вже і нема.
Як би це не звучало, але, я вважаю, що треба знаходити собі якісь заняття, щоб утримувати свою психіку в належному стані, наскільки це можливо. І якщо я зрозумію перші важкі тижні, то зараз, на третьому місяці війни, потрібно інколи відпочивати (але не забувати, що війна досі триває).
Тому я починаю думати про те, щоб повернутись до ведення цього каналу. Це буде така, невеличка часточка заняття, яка буде мене заспокоювати інколи. Можливо, вже скоро, я почну писати якісь технічні пости. Але є але.
Я поняття не маю про що я буду писати. В мене є ідеї в беклозі, я можу їх дописати, але потім я не знаю що буде. Можливо, не буде ніяких постів про TypeScript. Можливо, я почну писати про кібербезпеку трішки. Взагалі поняття не маю, коротше.
Тому в мене буде до вас прохання. Допоможіть мені зрозуміти, чи буде вам цікаво, якщо я почну писати пости не обмежуючись TypeScript-ом? Це все ще канал про програмування, комп'ютери й таке сяке, але може б не обмежуватись чимось одним? Якщо, на мою думку, буде підійматися якась цікава тема навколо програмування, компіляторів, ОС, кібербезпеки чи ще чогось, то я міг би про це писати. Що ви скажете?
Для того, щоб ми могли це проговорити, я повернув коментарі. Розраховую на те, що ватна аудиторія відписалась ще з часів мого попереднього посту. Тому є надії, що можна буде спокійно подумати, не намагаючись банити нікого (це виявилось не так просто). Але якщо все ж таки знову підніметься і буде нестись, то можливо коментарі ще рано вмикати 🙂
Коротше, думка кожного з вас важлива, буду радий її побачити в коментарях. Як на мене, то ідея з розширенням тематики непогана, а для вас?
Type Classes
Всем привет! Хочу поднять сегодня последнюю тему из запланированных по системам типов. В прошлый раз мы говорили об алгебраических структурах, как о концепции, "интерфейсах", что можно сделать над множеством элементов. А сегодня, мы поговорим о классах типов.
Что такое "классы типов"?
Классы типов это конструкция системы типов, которая позволяет вам создавать типы, содержащие в себе другие типы (я пытался перефразировать, честно). Да, как обычные классы, к которым мы привыкли, только для типов.
Подчеркну, что в отличие от алгебраических структур, которые являются чем-то абстрактным, классы типов это вполне реальная возможность системы типов в языке программирования. Они или есть в языке, или их нет. Это уже не про шаблоны поведения и не про контракты.
Давай пример, опять поток сознания
В прошлый раз, мы рассматривали алгебраические структуры на примере функтора. Через неё мы выражали необходимость в наличии функции map().
Эту алгебраическую структуру мы реализовали в TypeScript используя интерфейс. А вот в других, более функциональных ЯП, это реализовывается как раз через классы типов. На примере Haskell, это выглядит так:
class Functor f where
map :: (a -> b) -> f a -> f b
Мы говорим, что у нас есть функтор, в котором должна быть функция map(). И мы накладываем ограничения на map(), говорим что она должна работать так, как, собственно, работает map() (ваш капитан). Ну описали и что такого?
Параметрический полиморфизм
Всё это не имело бы смысла писать, если бы не один момент с классами типов. А именно, благодаря им можно реализовать параметрический полиморфизм. Что это такое?
Мы все знаем, что мы не можем сделать две функции map() в одной области видимости в TypeScript\JavaScript. Ошибкой будет наличие дублирующих идентификаторов. Но с помощью классов типов, можно указывать сколько угодно функций map(). До тех пор, пока сама сигнатура функции будет отличаться по типам.
Компилятор, в котором есть их поддержка, на этапе компиляции сможет понять какой из map() ему нужно вызвать, основываясь на типах параметров. Поэтому это и называется параметрический полиморфизм.
На примере того же функтора выше, мы можем добавить сколько угодно функций map(), но для компилятора это всё будет одна функция map(), полиморфизм которой будет достигаться за счёт отличия в параметрах.
Итог
Вот такая небольшая заметка о классах типов (Type Classes). Это одна из возможностей языка (Haskell, Scala?), которая используется для реализации алгебраических структур. Конечная цель такой конструкции - параметрический полиморфизм.
Пишите в комментариях, сильно ли вы "рады" тому, что я немного отклонился от TypeScript? Мне эта тема показалась интересной, поэтому захотелось поделиться с вами. А пока, больше мне по этой теме нечего будет добавить, так что я вернусь к постам около TypeScript.
P.S. И, да, я не привёл полезных кейсов и примеров с TypeScript, потому что в нём нету классов типов 🤷♀️
Алгебраические структуры
Всем привет! Появилось желание немного поговорить о системах типов, и в частности, поговорить об алгебраических структурах. Не то, чтобы я очень хорошо разбирался в этой теме, но попробую рассказать то, как я это понимаю, а мы обсудим в комментариях. Надеюсь, это поможет лучше структурировать знания и больше узнать.
Что за зверь?
Хочу начать с самого простого. Что такое вообще "алгебраическая структура"? Алгебраическая структура - это свод правил, который определяет, какие операции возможны на элементах из условного множества элементов. Это если говорить "по определению". Лично мне, мало что понятно из такого определения. Есть какое-то множество элементов и есть какая-то структура, которая определяет операции над этими элементами.
Поэтому я вдался в поиск аналогий. Мне понравилось как James Sinclair провёл аналогию с шаблонами проектирования. Шаблоны проектирования появились благодаря наблюдению за проблемами и выведению общих шаблонов решения этих проблем. Аналогично и с алгебраической структурой. Алгебраические структуры представляют собой общие шаблоны решения проблем. Но разница между ними в том, что алгебраические структуры имеют базис в виде математики. В то время как шаблоны проектирования это просто наблюдения.
Пример бы...
Абстрактно об этом проговорили, скорее всего всё ещё ничего не понятно. Давайте приведём примеры алгебраических структур, так будет лучше. Все же слышали эти модные словечки "функтор", "моноид" и прочее? Так вот это и есть те самые алгебраические структуры.
Если смотреть на алгебраическую структуру "Functor", то она нам говорит, что должна быть операция - map(). Это операция в качестве аргумента получает функцию, которая берёт что-то типа А и возвращает что-то типа B. На примере TypeScript-а это выглядело бы как-то так:
interface Functor<A> {
map<B>(fn: (a: A) => B): Functor<B>;
}
И если наш объект реализует данный свод правил (интерфейс), то мы можем сказать, что наш объект является функтором. Но не одним же функтором живём.
Их много
Алгебраических структур на самом деле много: функторы, апликативы, моноиды, профункторы и так далее. Это всё алгебраические структуры - свод правил, описывающий возможные операции над элементами.
Это становится намного проще понимать, если говорить об этом как об "интерфейсах" над множествами элементов. Разумеется, это "не по терминологии", за такое мне могут поломать руки, но как в попытках отыскать аналогии для лучшего понимания, я считаю вполне ок.
Пишите в комментариях, как вы понимаете термин "алгебраическая структура"? Согласны ли вы с этими аналогиями или они всё же принципиально разные? В следующих постах я бы ещё хотел поднять такие темы как тайп-классы и алгебраические типы данных, так что будут пересекаться с этим постом.
Type Challenges: EndsWith
Проблема
Реализовать тип EndsWith<T, U>, который проверяет, заканчивается ли строка T на строку U. Например:
type R0 = EndsWith<'abc', 'bc'> // true
type R1 = EndsWith<'abc', 'abc'> // true
type R2 = EndsWith<'abc', 'd'> // false
Решение
Не уверен, что эта проблема должна находится в категории среднего уровня сложности. Это больше похоже на легкий уровень сложности, чем средний. Но да ладно, кто я такой, чтобы судить.
Нам нужно проверить, что строка заканчивается на указанную подстроку. Раз в деле замешаны строки, значит нам точно будут полезны строчные тип литералы.
Давайте начнем с простейшего строчного тип литерала, который отображает все возможные строки. Так как нам содержимое, пока что, не интересно, используем тип any:
type EndsWith<
T extends string,
U extends string
> = T extends `${any}` ? never : never
С помощью этого выражения мы говорим компилятору проверить, присваиваемый ли тип T к any. Что, конечно же, так.
Теперь, давайте добавим нашу подстроку в решение. Нам нужно проверить что подстрока U находится в конце строки T. Давайте так и сделаем:
type EndsWith<
T extends string,
U extends string
> = T extends `${any}${U}` ? never : never
Используя такую конструкцию, мы проверяем, что строка присваиваемая ко всему, но что должно заканчиваться на U - просто. Всё что остается это вернуть true или false литералы в зависимости от результата:
type EndsWith<
T extends string,
U extends string
> = T extends `${any}${U}` ? true : false
Что почитать
- Условные типы
- Строчные тип литералы
Web-версия
https://ghaiklor.github.io/type-challenges-solutions/en/medium-endswith.html
Языки программирования и спецификации
Давайте сегодня поговорим о языках программированиях, а именно, о спецификациях и какую роль они выполняют. Я обойдусь без своей личной интерпретации, просто насыпем немного фактов, а вы уже сами решайте - хорошо это или плохо.
Наверняка многие из вас слышали о таком понятии как "формальная спецификация языка программирования". Что же это значит?
Спецификация языка программирования это документ, в котором описано как язык должен себя вести. Как он должен выглядеть с синтаксической точки зрения. Какие семантические свойства даются тем или иным конструкциям языка. И так далее... Другими словами, спецификация это формальный контракт между пользователями языка и авторами компиляторов, которые реализуют этот язык.
Представим ситуацию. Вы - автор компилятора, язык которого формально специфицирован. То есть у вас есть модель поведения языка, вам лишь остается её реализовать у себя. А пользователь вашего компилятора может пойти в спецификацию и согласно ей, написать программу, которая будет ожидаемо выполняться вашим компилятором. Пока обе стороны соблюдают описанную модель - всё будет отлично.
Приведу примеры языков программирования, у которых есть спецификации:
- JavaScript
- PHP
- Java
- C++
В любой непонятной ситуации, вы можете пойти в спецификацию и посмотреть, а как же должен вести себя код. Выглядит как бонусы сплошь и рядом и спецификации нужны всегда, но... не совсем.
Спецификации нужны не всегда и иногда они даже берут больше ресурсов на описание и поддержку, чем фактическую пользу от них. Более того, попытки сначала формализовать язык и только после формализации - реализовать, приводят к уйме проблем. Например, не удается реализовать то, что было формализовано (читай как, придумано "художниками") и спецификацию приходится "подправлять" под реализацию.
На сегодняшний день, языки и их спецификации принято развивать параллельно.
Развитие спецификации формализует поведение компилятора, а развитие компилятора проверяет что спецификация реализуемая.
И довольно много языков начинались вовсе без спецификации и только спустя определенное время она у них появлялась (тот же PHP, Python, даже у TypeScript была попытка). При этом, некоторые языки до сих пор живут без спецификации и им не мешает это ничуть. Например, Rust и Typescript.
Чем аргументируется отсутствие спецификации для языка программирования?
В случае, если язык не планируется быть встраиваемым и при этом у этого языка только один компилятор - чаще всего обходятся так называемой референтной реализацией.
Референтная реализация это тот случай, когда работает авторитаризм. Авторы языка, разрабатывающие компилятор, надают ему "авторитарный статус" и сам компилятор считается "единственно верной" реализацией языка. В случае, если вы попадаете на неопределенное поведение, то это либо баг в референтной реализации или ожидаемое поведение языка.
Проверяются такие референтные реализации наборами тестов с небольшими кусками кода и проверкой ожиданий, что пример отработал как от него и ожидалось. Без шуток, это самые обычные тесты в обычном понимании этого слова. Вы кидаете строку с текстом программы в компилятора, запускаете её и проверяете ожидания. Вот и всё.
Таким образом у языков без спецификации есть "авторитарная" реализация и набор тестов, проверяющий, что компилятор ведёт себя соответствующе.
Оба варианта имеют право на жизнь, в зависимости от ситуации и ожиданий конечно же. Оба варианта имеют свои сильные и слабые стороны и всегда нужно оценить обстоятельства и принять правильное решение, а что же вам лучше подойдёт. Не будьте категоричными ✌️
TypeChallenges: Percentage Parser
Проблема
Реализовать тип PercentageParser<T extends string>. Этот тип должен разбить строку на три элемента, согласно /^(\+|\-)?(\d*)?(\%)?$/.
Структура этих элементов выглядит следующим образом: [+ или -, число, %]. В случае, если совпадения нет, нужно вернуть пустую строку. Например:
type PString1=''
type PString2='+85%'
type PString3='-85%'
type PString4='85%'
type PString5='85'
type R1=PercentageParser<PString1> // expected ['', '', '']
type R2=PercentageParser<PString2> // expected ["+", "85", "%"]
type R3=PercentageParser<PString3> // expected ["-", "85", "%"]
type R4=PercentageParser<PString4> // expected ["", "85", "%"]
type R5=PercentageParser<PString5> // expected ["", "85", ""]
Решение
Синтаксический разбор строк это очень интересная задача (как для меня). Жаль только, что мы не сможем добиться хорошего решения в этом случае. Так как всё что у нас есть это только система типов TypeScript.
Нам нужно разбить строку на три компонента: знак числа, число, знак процента. Чтобы упростить решение, давайте реализуем их как отдельные типы. Первый тип будет возвращать знак числа. Второй будет возвращать само число, а третий - знак процента.
Начнём с первого типа. Нам нужно проверить, что первый символ в строке это знак плюса или минуса. Чтобы этого достичь, нам нужно сначала вывести этот первый символ.
type ParseSign<T extends string> = T extends `${infer S}${any}`
? never
: never
Имея первый символ в тип параметре S, мы можем проверить плюс это или минус. Если это плюс или минус, то возвращаем тип параметр S, возвращаем знак, что мы вывели. Во всех остальных случаях возвращаем пустую строку, согласно постановке задачи.
type ParseSign<T extends string> = T extends `${infer S}${any}`
? S extends '+' | '-' ? S : ''
: ''
Таким образом, мы реализовали тип, который может распознать и вернуть знак числа. Теперь, сделаем то же самое со знаком процента. Для начала, проверим, а есть ли символ процента в конце строки.
type ParsePercent<T extends string> = T extends `${any}%`
? never
: never
В случае, если символ процента присутствует в конце строки - возвращаем символ процента. Во всех остальных случаях возвращаем пустую строку.
type ParsePercent<T extends string> = T extends `${any}%`
? '%'
: ''
Имея два типа, два анализатора, которые возвращают знаки, мы можем начать думать о числе. Нам нужно вывести число, которое стоит между этими знаками. Проблема заключается в том, что эти знаки опциональные.
Чтобы реализовать поддержку опциональных знаков, нам нужно проверять их наличие. То есть нам нужно проверять, если присутствует знак числа, то пропустить его и взять число. И так далее. А проблема в том, что если мы этого не сделаем, то в наше число попадут ненужные нам знаки.
Но у нас же эта логика уже реализована в наших других типах. Всё что нам нужно это правильно их совместить.
type ParseNumber<T extends string> = T extends `${ParseSign<T>}${infer N}${ParsePercent<T>}`
? never
: never
Видите что происходит? Сначала, мы анализируем случай со знаком числа, используя ранее написанный тип. Если знак числа присутствует, тип его возвращает и делает частью тип литерала. А значит, он не попадет в часть, которая выводит число.
То же самое мы проделываем и со знаком процента. Если процент присутствует, тип его нам возвращает и делает частью строчного тип литерала. Это не дает ему попасть в часть с выведением числа.
В результате мы остаемся только с самим числом, которое мы и получаем через выведение типов. Нам остается его только вернуть из условного типа.
type ParseNumber<T extends string> = T extends `${ParseSign<T>}${infer N}${ParsePercent<T>}`
? N
: ''
Вы, наверняка, уже догадываетесь, как мы можем это использовать для решения задачи. Как сказано в условии, нам нужно вернуть кортеж с тремя элементами. А у нас как раз есть три типа!
type PercentageParser<A extends string> = [
ParseSign<A>,
ParseNumber<A>,
ParsePercent<A>,
]
Мои поздравления! Мы получили простейший "синтаксический анализатор" с использованием системы типов.
Type Challenges: Push
Disclaimer
Всем привет! Сейчас очень много всего происходит и у меня физически не хватает времени на "подумать" о постах и уж тем более их написать. Идей становится всё больше, а писать их некогда.
Например, сегодня вечером у меня будет доклад, на следующей неделе планирую поездку и так далее и тому подобное. Но у меня хватило немного времени решить очень простую задачку из Type Challenges.
Почти уверен в том, что смогу вернуться к старому графику по 1-2 поста в неделю уже с нового года. А пока... Type Challenges.
Проблема
Реализовать функцию Array.push на уровне системы типов. Например:
type Result = Push<[1, 2], '3'> // [1, 2, '3']
Решение
И вправду, легкая задача. Чтобы реализовать тип, который добавит новый элемент в существующий массив, нам нужно сделать два действия. Первое - это взять все элементы из входного массива. Второе - это добавить к этим элементам новый.
Чтобы взять все элементы из массива, мы можем воспользоваться вариативными типами. Давайте это и сделаем и вернем массив с элементами из массива T:
type Push<T, U> = [...T]
Получаем ошибку компиляции "A rest element type must be an array type". Это означает, что наши вариативные типы могут работать только с массивами. Поэтому добавим ограничение на дженерике и скажем, что мы работаем с массивами:
type Push<T extends unknown[], U> = [...T]
Теперь, у нас есть копия входного массива T. Единственное, что остается, это добавить к этим элементам элемент U:
type Push<T extends unknown[], U> = [...T, U]
Таким образом, мы реализовали операцию добавления нового элемента в массив на уровне типов.
Что почитать
- Дженерики
- Вариативные типы
Web-версия
English - https://ghaiklor.github.io/type-challenges-solutions/en/easy-push.html
Русский - https://ghaiklor.github.io/type-challenges-solutions/ru/easy-push.html
Українська - https://github.com/ghaiklor/type-challenges-solutions#how-to-contribute
CFA для объединений с дискриминантом
27 дней назад, Anders Hejlsberg залил очень важный Pull Request в основную ветку TypeScript-а. Можно будет использовать type guards на объединениях с дискриминантом и TypeScript будет понимать, что мы проверили один из вариантов 🎆
О каких объединениях речь?
Мы как-то уже говорили о них в прошлом посте (/channel/wild_wild_web/84). Это объединения, у которых есть одно общее поле и оно является дискриминантом на этом объединении. То есть, по значению одного только поля (дискриминанта), компилятор может автоматически вычислить структуру типа.
Давай пример, ничего не понятно
Вот скажем, у вас есть такой тип:
type Action =
| { kind: 'A', payload: number }
| { kind: 'B', payload: string };
В данном случае, дискриминантом этого объединения будет поле kind. И каждый раз, когда мы "доказывали" компилятору, что у нас kind это A, то он автоматически выводил тип payload в number. Аналогично, если мы "доказывали" компилятору, что у нас kind это B, то он выводил тип payload в string.
Вроде всё понятно, мы об этом уже говорили, так в чем соль?
Чем этот Pull Request важен?
Если у вас была функция, которая принимала это объединение Action как параметр, например, то даже с использованием type guard, тип не сужался. Например:
function f11(action: Action) {
const { kind, payload } = action;
if (kind === 'A') {
payload.toFixed();
}
if (kind === 'B') {
payload.toUpperCase();
}
}
В данном случае, payload внутри условных веток всё равно был объединением и вы не могли использовать методы чисел или строк на них без дополнительных проверок typeof и прочее.
С появлением Control Flow Analysis (CFA) для таких случаев, теперь это будет работать! Вы сможете с использованием type guard-ов сужать объединения к нужным типам по их дискриминанту.
Лично у меня, это просто уйма кода уйдёт в никуда, потому что наконец-то TypeScript прокачал CFA для таких случаев. Пишите в комментариях, а часто ли вы используете объединения с дискриминантом?
P.S. Я не знаю, когда это собираются уйти в релиз, но предположу что очень скоро, учитывая что оно уже в основной ветке.
Rush
Гигантская машина с высоким порогом входа - всё что вам нужно о нём знать. Если у вас нет проблем с текущими монорепами - он вам не нужен. Но, давайте всё же чуть детальнее.
Скажем, у вас огромная монорепа. Вы уже поняли что без подхода "One Version" вы просто утонете в менеджменте зависимостей. Локальные билды, чтобы проверить своё изменение в одном пакете, проходят по 30-40 минут. И так далее и тому подобное, рассказывать можно очень долго.
Как только у вас появляются подобные проблемы, скорее всего вам пора задуматься о более серьезном инструменте для управления. Один из них это как раз Rush.
Он предоставляет вам из коробки инкрементальную компиляцию, то есть ничего не будет собираться, если оно не изменялось. Скажем, вы собрали пакет А и пакет Б. Rush построил отпечаток этих пакетов и записал в своё состояние. После, вы пошли и поменяли пакет Б и снова запустили билд. Rush посмотрел, что отпечаток пакета А не изменялся, а значит артефакт будет тот же. Следовательно, билд можно пропустить, сэкономив уйму времени.
Транзитивные зависимости ваших пакетов становятся недоступными для пользования. Когда репозитории маленькие, то все зависимости в одной свалке не очень большая проблема. Но когда это превращается в свалку больше и больше, то энтропия и шанс возникновения упавшего билда из-за какой-то транзитивной зависимости возрастает. Все зависимости которые использует ваш пакет должен быть строго объявлен в манифесте.
С его помощью можно сделать настоящие дистрибутивные билды на CI. То есть, одну часть монорепы собирать на одном кластере, а вторую часть монорепы собирать на втором кластере. Для Rush нет необходимости иметь всю монорепу в полном составе. Он так организовывает файловую структуру, что все пакеты являются самодостаточными и их можно собирать отдельно от всей монорепы.
Я уже так много расписал, поэтому остановлюсь и подчеркну следующее. Кривая входа в Rush большая, без шуток. Для большинства из вас он вам не нужен, скорее всего. И вы только дольше будете мучиться с его настройкой, чем с проблемами, которые он решает. Но как только вы поймете, что у вас начались проблемы с монорепой - возможно пора посмотреть на инструменты типа Rush.
Другие инструменты?
Я описал только те, с которыми мне пришлось поработать и я на них не одну монорепу собрал. Уверен, что есть и другие инструменты, например Nx.js. Пишите в комментариях, чем вы собираете свои монорепы или вам повезло работать по "классической" схеме? Какие инструменты есть, может я о каких-то не знаю и есть что-то лучше или хуже?
Дискуссия
И, к слову, раз уже подняли эту тему, то оставлю ссылку на нашу дискуссию во время Framework Days. Там участвовали разные люди с разным опытом и каждому было что сказать по поводу монореп и стоит ли с ними жить.
TypeScript Configuration
Недавно я делал пост о моей личной подборке плагинов к ESLint-у и меня попросили сделать похожее о конфигурации TypeScript. Сегодня решил посвятить это утро моей личной подборке флагов к нему.
Стоит уточнить, что это простой мой личный выбор и набор флагов, которые, по моему мнению, должны быть включены. Поэтому не расценивайте этот пост как "вот так правильно, а так нет". Смотрите на него как на "вот у других людей так, может и у меня сработает или буду хотя бы знать, что такое есть".
- allowUnreachableCode (false) - бросает ошибки компиляции, если у вас есть код, который никогда не будет выполнен.
- alwaysStrict (true) - всегда добавлять к сгенерированному JS файлу директиву "use strict". Это позволяет движкам выполнять JS в строгом режиме (по умолчанию это не так) и имеет свои преимущества, но это отдельная тема.
- no* (true) - семейство флагов, которое начинается с no это флаги линтера. Тут решайте сами для себя, что включить, а что пропустить, но большинство флагов очень полезные. Например, noImplicitAny, noImplicitThis и подобные я настаиваю на включении всегда. Они позволяют избежать неявных операций над типами, как например неявное присваивание типа any к переменной.
- strict (true) - переводит компилятор в строгий режим, который проверяет типы более строго. Например, strictNullChecks начинает учитывать типы null и undefined, заставляя вас покрывать случаи, когда данных может и не быть (по умолчанию, эти типы не учитываются компилятором). Или useUnknownInCatchVariables, который заставляет вас использовать тип unknown на брошенной ошибке. Следовательно, будет требовать от вас проверки, какой класс ошибки был брошен.
- declaration и declarationMap (true) - вместе с JS-ом генерирует и d.ts файлы, которые описывают ваши интерфейсы, сигнатуры функций и прочее. А declarationMap позволяет по d.ts файлам находить путь к исходному коду в .ts файле.
- sourceMap (true) - помимо генерации d.ts файлов и их "карты", дополнительно можно ещё генерировать "карты" для JS файлов. Это помогает работать с отладчиком, расставлять точки останова в TS файлах и при этом отлаживать JS код.
- esModuleInterop (true) - на текущий момент у нас много модулей написанных в CommonJS и (уже) достаточно много модулей написанных по EcmaScript Modules спецификации. Проблема в том, что эти модули между собой несовместимы и в некоторых случаях могут приносить ненужные проблемы. Поэтому есть этот флаг, который дополняет JS код небольшими вспомогательными функциями, предоставляющими interoperability между этими двумя системами модулей.
- target (ES2020) - последняя LTS версия Node.js это Node.js 14.x, которая полностью поддерживает ES2020. Нет смысла компилироваться в более старые версии спецификации, если только у вас не более старый Node.js (или браузер). Если вы хотите быть уверенными в выборе этого поля, то можете пойти на этот сайт и увидеть таблицу совместимости разных версий спецификации с разными рантаймами.
- skipLibCheck (true) - наличие этого флага пропускает проверку типов в ваших зависимостях. Подразумевается, что они уже проверены авторами этих зависимостей, поэтому проверять их ещё раз на вашей стороне не имеет смысла. Это уменьшает время компиляции и избавляет вас от сценариев конфликтующих типов из разных зависимостей.
Вот такой вот список, по моему мнению, самых интересных флагов, которые вы можете указать в tsconfig.json. Если у вас есть время и желание посмотреть на полный список, то его вы можете найти по этой ссылке.
🔄 Node.js v17 🔄
Два дня назад подготовили первую релизную сборку Node.js v17. Из значимых изменений завезли следующее:
- OpenSSL 3.0 - сам я не силен в криптографии, но пишут что добавили поддержку QUIC и FIPS модулей. Об этом подробнее можно почитать здесь.
- V8 9.5 - улучшения в Intl API и поддержка обработки исключений в WebAssembly. Подробнее о них здесь.
- readline - добавили Promise API. Теперь можно импортировать readline/promises и await-ить интерактивности с пользователем.
- npm 8.1.0 - в поставке с этой версией Node.js идёт npm версии 8.1.0.
Список остальных мелких изменений можно посмотреть у них в репозитории. Также стоит напомнить, что нечетные версии Node.js не являются и не будут являться версиями с долгоживущей поддержкой (LTS). Поэтому тащить её в прод не советую, но поиграться можно.
Type Challenges: Drop Char
Проблема
Удалите указанный символ из строки. Например:
type Butterfly = DropChar<' b u t t e r f l y ! ', ' '> // 'butterfly!'
Решение
Чтобы решить эту проблему, нужно знать, что такое шаблонные тип литералы в TypeScript. Вы можете более подробно о них почитать на их сайте.
Когда мы используем шаблонные тип литералы, мы можем выводить части строк этого литерала. Воспользуемся этим, чтобы вывести левую и правую части литерала. А разделителем будет сам символ, который нам нужно удалить.
type DropChar<S, C> = S extends `${infer L}${C}${infer R}`
? never
: never;
С такой записью, мы получим ошибку компиляции Type ‘C’ is not assignable to type ‘string | number | bigint | boolean | null | undefined’.
. Добавим ограничение на дженериках, чтобы этого избежать.
type DropChar<S, C extends string> = S extends `${infer L}${C}${infer R}`
? never
: never;
Теперь, мы видим что у нас есть две части литерала и отдельно разделитель. Так как нам нужно разделитель удалить, то мы возвращаем только левую и правую части.
type DropChar<S, C extends string> = S extends `${infer L}${C}${infer R}`
? `${L}${R}`
: never;
Таким образом, мы удалили один символ из строки. Чтобы продолжить удалять остальные, вызовем наш тип рекурсивно.
type DropChar<S, C extends string> = S extends `${infer L}${C}${infer R}`
? DropChar<`${L}${R}`, C>
: never;
Мы покрыли все случаи, кроме случая, когда совпадений по шаблону нет. Если такая ситуация приключилась, вернём входную строку без изменений.
type DropChar<S, C extends string> = S extends `${infer L}${C}${infer R}`
? DropChar<`${L}${R}`, C>
: S;
Что почитать
- Шаблонные тип литералы
- Условные типы
- Выведение типов в условных типах
Web-версия
English - https://ghaiklor.github.io/type-challenges-solutions/en/medium-drop-char.html
Русский - https://ghaiklor.github.io/type-challenges-solutions/ru/medium-drop-char.html
Українська - https://ghaiklor.github.io/type-challenges-solutions/uk/medium-drop-char.html
Откуда идёт вся эта история с типами?
Один лагерь топит за то, что типы не нужны. Вот на Ruby набросать, на Python набросать, сверху присыпать JavaScript-ом и в прод. Всё работает, код пишется, работа идёт и не мучаешься с какими-то ошибками компиляции.
Второй лагерь топит за то, что без типов и жизнь не та. Вот на Go набросать, на Rust набросать, сверху TypeScript-ом присыпать и в прод. Тоже всё работает, код пишется, работа идёт и не мучаешься с undefined is not a function.
Но откуда вообще пошли эти типы и почему типы? Системы типов у разных языков совершенно разные, но у всех есть одно общее зерно - теория типов.
Теория типов - это академическое направление и только академическое направление. Её используют во всяческих математических задачах, доказательствах, там, где нужно формально что-то доказать. Причем теория типов не одна. Есть теория типов "lambda calculus", есть "intuitionistic type theory" и так далее.
Математики используют эти теории типов с целью формально доказывать теоремы и уж точно не писать код (в привычном для нас понимании, так-то они тоже пишут код, просто с использованием других языков).
И как это обычно бывает, есть мир теории, где варятся академики и математики. И есть мир практики, в котором живем мы с вами, разработчики или инженеры (кому как удобно себя называть). Авторы языков программирования, которыми мы с вами пользуемся, очень тесно дружат с теорией типов и полностью или частично реализовывают её у своих языках - своих системах типов.
Эта система типов - это логическая система, в которой действуют правила, заданные авторами. Если авторы решили, что в языке может существовать тип "строка" и над этим типом можно выполнять операции объединения строк и выборки подстрок, то эта информация пойдёт в систему типов языка. Если решили, что есть число и его можно только суммировать с другим числом - это тоже информация для системы типов конкретного языка. В общем, закономерность прослеживается.
В конечном счете, когда программа компилируется, у каждой вашей переменной или функции вычисляется тип или берётся тот, что вы указали в коде программы. Этот тип обозначает какие операции можно над ним производить и если вы попытаетесь произвести над типом операцию, которая недопустима к нему - логическая ошибка в системе типов. Следовательно, ошибка компиляции - в вашем коде засела логическая ошибка. Причем эта логическая ошибка является ошибкой только в конкретной системе типов. Далеко не факт, что такая же логика приведёт к логической ошибке в другой системе типов.
И напоследок, могу добавить, что как бы вы не топили за один лагерь или второй - все языки программирования используют системы типов. Просто в некоторых ситуациях, эти системы типов скрыты от конечного разработчика во внутренностях самого языка.
Вот такой вот пятничный пост на "обсудить" в компании коллег, друзей или знакомых. Чуть ниже я оставлю ссылок по теме, а вы пишите в комментариях, приходилось ли вам сталкиваться с разными системами типов? Если да, то какая система типов какого языка понравилась больше всего? А если не сталкивались, то почему не хотите попробовать, это бесплатно 😜
Небольшой disclaimer: это всё довольно обобщенная информация, которую я смог собрать и понять. Тонкостей и деталей я не знаю, но я в процессе изучения этой темы.
- Type Theory
- Typed Lambda Calculus
- Intuitionistic Type Theory
- Type System
- Where Do Type Systems Come From?
У меня как-то спросили, а есть ли смысл держать исходные коды вашего проекта на TypeScript в npm пакете? Или, например, в Docker образе? И это, на самом деле, хороший вопрос, над которым я раньше не задумывался.
С одной точки зрения, держать исходные коды проекта в его результирующем билде не имеет никакого смысла. В случае с TypeScript, JavaScript считается его целевым языком, следовательно, в билде должен присутствовать только целевой язык - JavaScript. Но точно не исходный код из которого этот целевой язык был скомпилирован. Это как если бы вы брали C++, Go или Rust и к артефактам компиляции добавляли ещё и исходные коды самого проекта на C++, Go или Rust. Как по мне, это выглядит лишенным смысла.
С другой точки зрения, иметь исходные коды проекта вместе с информацией для отладчика упрощает процесс отлова ошибок. Каких-то других аргументов для этой точки зрения я придумать не могу (пишите в комментариях, что вы думаете, может есть ещё какие-то плюсы тянуть исходные коды?).
В конечном счете, я решил использовать следующую схему сборки релизов. Во всех наших проектах есть папка src, где лежат все исходные коды проекта. В момент компиляции TypeScript-ом в целевой язык, всё что лежит в src полностью зеркалируется в папку dist. Другими словами, мы интерпретируем папку dist как релизную сборку с информацией для отладчика (source maps) и не тянем никаких исходных кодов.
Позже, в зависимости от типа артефакта, мы указываем папку dist и только её. Если это npm пакет, то мы в package.json указываем dist в files. Если это Docker образ, то мы копируем только папку dist в него. И так далее...
Не уверен, что много кто делает это по другому (вроде как общепринятый стандарт), но если у вас другая схема и у вас есть аргументы, почему исходники бывают полезными в релизной сборке, напишите в комментариях.
Сегодня у нас "лёгкий" пост, решил ответить на вопрос от подписчика. И вот смотрю сейчас в беклог и вижу ещё один вопрос от подписчика по поводу типов. Где они полезны, где не очень, и так далее - холиварная тема. Возможно, напишу своё мнение на этот счёт. Так как обстоятельства не бывают "бинарными", сценарии применения типизированных языков также не "бинарные". Постараюсь раскрыть эту тему 😉