wild_wild_web | Unsorted

Telegram-канал wild_wild_web - Wild Wild Web

-

Полезные заметки о TypeScript и обо всем, что его окружает

Subscribe to a channel

Wild Wild Web

Привет! Это небольшой пост, призванный осветить происходящее в Украине 🇺🇦

24 февраля, рано утром, Россия открыла огонь по нескольким мирным городам Украины. Я был вынужден покидать свой дом под звуки сирен авиационной тревоги. Сегодня 27 февраля, полномасштабная война всё ещё идёт полным ходом. Россия нарушает все возможные конвенции, открывает огонь по мирным жителям, расстреливает мирных граждан в спины, не дает возможности организовать "зеленый коридор".

Поэтому, сообщаю следующее. Это мой последний пост на этом канале на русском языке. Если вы не согласны с политикой Путина - выходите на протесты. Если вы не согласны с моей политикой - отписывайтесь от этого канала. Если вы хотите высказаться о "я же не нападаю, это всё власть" - даже не пытайтесь, я буду игнорировать такие комментарии.

UPD: вимкнув коментарі, тому що русні багацько
UPD2: видавил групу обговорень, так як вони і туди знайшли шлях

Читать полностью…

Wild Wild Web

Алгебраические типы данных

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

Что у нас по определению?
Как оказалось, сложных определений здесь нет. Алгебраические типы данных - это типы, собранные из других типов данных. Всё. Буквально. Это конец определения.

Я почти уверен, что вы все используете алгебраические типы данных, но не подозреваете об этом. Или подозреваете, если вы знакомы с этим, конечно же. И, да, давайте дальше будем использовать сокращенную форму 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 формат, может увидемся.

Читать полностью…

Wild Wild Web

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-ов и декораторов - полностью поддерживаю.
- Не навязывайте вкусовщину (кроме своих коллег, конечно же 🙃).

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

Читать полностью…

Wild Wild Web

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

Читать полностью…

Wild Wild Web

Что почитать
- Условные типы
- Выведение в условных типах
- Строчные тип литералы

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

Читать полностью…

Wild Wild Web

ChowJS: AOT JavaScript Engine

Попалась мне статья о ChowJS, в которой рассказывается опыт о создании AOT компилятора для JavaScript. Довольно интересно, но не вдаваясь в технические детали реализации, рассказывают о подходах, которые они применили.

Этот компилятор в основном базируется на движке QuickJS (это один из самых маленьких движков для JavaScript, которые легко встраивать в свои приложения). Они же его и используют для того, чтобы интерпретировать JavaScript.

Загвоздка в том, что они его интерпретируют только один раз, при самой компиляции. Чаще всего, после запуска вашего JavaScript кода, прототипы не меняются, глобальные переменные не меняются и так далее. Основываясь на этом предположении, ChowJS один раз интерпретирует JavaScript с помощью QuickJS и уже после, использует полученный контекст выполнения для дальнейшей компиляции.

Это можно выразить в таком простеньком алгоритме:

- Запускаем JavaScript в QuickJS движке для его изначального запуска и инициализации
- Для каждого метода получаем байт-код и трансформируем его во внутреннее представление ChowJS
- Применяют оптимизирующий компилятор к этому IR (внутреннее представление)
- Получившееся представление компилируют в код на С, который в последствии и компилируют в машинный код

В целом очень интересная статья, рекомендую к прочтению. Дальше по статье автор разбирает байт-код, внутреннее представление, делится идеями о будущем этого инструмента.

Читать полностью…

Wild Wild Web

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

Читать полностью…

Wild Wild Web

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

Читать полностью…

Wild Wild Web

Монорепы и инструменты сборки

Всем привет! Просматривал сегодня утром беклог постов на канал и заметил такую тему как монорепы. Вспомнил, что у нас недавно ещё была и дискуссия на 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.

Читать полностью…

Wild Wild Web

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

Читать полностью…

Wild Wild Web

Всем привет! Давайте сегодня разбавим посты о 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

Читать полностью…

Wild Wild Web

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

Читать полностью…

Wild Wild Web

Всем привет! Сегодня решил поднять очень сложную тему. Сложная она по той причине, что объективно "правильного" ответа на неё нет. И можно даже поднять споры на тему того, что же считать "правильным" в этой теме. Поэтому, пишу жирными буквами, это просто моё личное мнение.

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

Поэтому, наличие инструмента, с помощью которого каждый участник разработки может оставить "полезную информацию" о своем решении, очень сильно упрощает жизнь его коллегам. Коллеги не лезут в "черную коробочку", они только знают о том какие у неё есть входы и какие выходы. А инструмент (компилятор) проверяет что все ваши ожидания всех ваших коллег соответствуют.

Более того, компиляторы обладают информацией о том, как ваш код будет выполняться (control flow analysis, data flow analysis). Он может проложить возможные пути и вывести из этого возможные сценарии, которые вы можете получить в конкретном месте. Это помогает не забывать обрабатывать разные ситуации или ошибки, которые, по вашему мнению, не могли бы случиться.

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

Где статическая типизация (не)полезная?
Это, конечно, лично выбор каждого, но если вы работаете один и проект у вас небольшой, то возможно и смысла тянуть статическую типизацию нет. Особенно, если это проект малой сложности и сильно много изобретать не придется.

Вторая сторона медали статической типизации заключается в том, что скорость разработки понижается. Скажем, вы хотели себе просто сделать CLI с двумя командами, которые бы запускали другие процессы. И вместо того, чтобы на том же JavaScript или Bash за 5 минут реализовать его, вы бы потратили несколько часов на объяснение компилятору, а что вы вообще хотите сделать.

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

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

А вы как думаете? Пишите в комментариях, когда бы вы использовали языки программирования со статической типизацией, а когда осознанно бы отказались от них?

Читать полностью…

Wild Wild Web

🎉 1 год каналу 🎉

Всем привет! Сегодня для этого канала важный день - ему исполняется один год. Ровно год назад, был написан первый пост. За последний год мы успели поговорить о TypeScript, местами о Node.js, иногда обсуждали вещи, которые никак не касаются TypeScript/Node.js, но их объединяло одно - веб.

И я не думаю, что стоит что-то менять из этого. Нас уже 1,000+ человек. Мы собрались все для того, чтобы следить за миром веб технологий и я буду стараться успевать за ним. Безусловно, основной упор будет идти на TypeScript, так как мы его активно используем на текущих проектах и нам есть что рассказать. Но ничего же не мешает нам поговорить и о других веб технологиях, правда?

Что наводит меня на мысль, что название канала немного не соответствует правде. И я его переименую в просто "Wild Wild Web".

Цель этого канала - делиться новостями из мира TypeScript, нашим опытом, который мы получаем в Wix, когда работаем с TypeScript, советами и прочее (you know the drill).

Но и не только в постах же дело. Помимо постов, есть ещё и люди, которые читают этот канал и могут оставлять комментарии, вести дискуссии. Кто-то читает потому что ему интересно наблюдать за миром языков, кто-то - активно использует TypeScript и хочет знать немного больше. Мотивации могут быть разные, но цель (ещё) одна - создать сообщество, которое может обмениваться знаниями.

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

Очень хочется добавить больше вариантов взаимодействия с постами, поэтому (возможно!) я буду экспериментировать с разными ботами, которые добавляют голосования, реакции, не знаю что ещё может там быть. Буду разбираться с этим. В итоге очень хочется получить систему, по которой вы бы сами могли предлагать темы на будущие посты, о чем вам интересно почитать.

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

На этом, думаю, можно и остановиться, пока что. Всем хорошего дня и поднимаю бокал за 1 год каналу, cheers 🍷

Читать полностью…

Wild Wild Web

Всем привет! Сегодня будет небольшой пятничный пост, полезность которого крайне мала, но зато интересно.

Данную возможность Node.js приходится использовать очень редко и в особенных случаях. Используя флаги V8, которые можно пробросить через Node.js, вы можете увидеть, а какой именно код получился на выходе. Как именно ваш JavaScript компилировался движком, какой байт-код вы получили и какой машинный код.

Для примера возьмем функцию, которая суммирует два числа:

function add(a, b) {
  return a + b;
}

add(2, 3);


Вызывая Node.js и передавая ему флаг print-bytecode, мы получим в консоли байт-код, который получился в результате работы Ignition интерпретатора. Вместе с флагом print-bytecode-filter, мы можем отфильтровать всякий мусор и сфокусироваться только на нашей функции.

Вызываем наш скрипт с этими флагами и смотрим на выход (node --print-bytecode --print-bytecode-filter=add test.js):

0 : 25 02 Ldar a1
2 : 34 03 00 Add a0, [0]
5 : aa Return


Видим байт-код нашей функции, где и происходит загрузка в аккумулятор второго аргумента и возврат суммы с первым аргументом.

Но можно пойти ещё дальше и посмотреть на сам скомпилированный машинный код. Для этого добавим с помощью цикла for больше вызовов нашей функции, чтобы Turbofan в V8 подключился в работу и скомпилировал наш код в машинный.

После, используя флаг print-code, можно получить машинный код нашего исходного текста программы. Я не буду приводить его в этом посте, так как он огромный, но поверьте на слово - он там есть 🙂

Есть ещё флаги print-opt-code и print-opt-code-filter, которые работают аналогично флагам print-bytecode и print-bytecode-filter, только с оптимизированным машинным кодом.

Подобьем списочек:

- print-bytecode - выводит в консоль байт-код вашего JavaScript кода
- print-bytecode-filter - выводит в консоль только тот байт-код, который соответствует имени указанной функции
- print-code - выводит в консоль машинный код
- print-opt-code - выводит в консоль оптимизированный машинный код
- print-opt-code-filter - выводит в консоль оптимизированный машинный код только для указанной функции

Что я хотел сказать этим постом? В Node.js можно использовать флаги, которые позволяют посмотреть на результирующий код ваших функций. Это очень полезно, когда у вас проблемы с производительностью и вы не понимаете в чем дело. В таких ситуациях помогает пойти посмотреть, во что JavaScript скомпилировался и может проблема в движке (а может и нет, кто знает).

Также, я писал о подобной тематике у себя в блоге, можете почитать. Разбавлю эту ссылку ещё парочкой полезных ссылок, которые затрагивают тему байт-кода и машинного кода в Node.js.

- Journey of the Code (мой блог-пост, где я прослеживаю путь от TypeScript кода до машинного кода)
- Understanding V8’s Bytecode (пост от Franziska Hinkelmann, которая рассказывает о том же 🙂)
- Ignition - an interpreter for V8 (запись доклада с BlinkOn, где рассказывают о том, что такое Ignition)

Пишите в комментариях, были ли у вас случаи, хотя бы раз (!), когда вы пользовались этими флагами не ради фана? Ожидаю, что ответ будет "нет", поэтому наброшу ещё один вопрос - как вы думаете, в какой ситуации это могло бы действительно пригодиться?

Читать полностью…

Wild Wild Web

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, потому что в нём нету классов типов 🤷‍♀️

Читать полностью…

Wild Wild Web

Алгебраические структуры

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

Что за зверь?
Хочу начать с самого простого. Что такое вообще "алгебраическая структура"? Алгебраическая структура - это свод правил, который определяет, какие операции возможны на элементах из условного множества элементов. Это если говорить "по определению". Лично мне, мало что понятно из такого определения. Есть какое-то множество элементов и есть какая-то структура, которая определяет операции над этими элементами.

Поэтому я вдался в поиск аналогий. Мне понравилось как James Sinclair провёл аналогию с шаблонами проектирования. Шаблоны проектирования появились благодаря наблюдению за проблемами и выведению общих шаблонов решения этих проблем. Аналогично и с алгебраической структурой. Алгебраические структуры представляют собой общие шаблоны решения проблем. Но разница между ними в том, что алгебраические структуры имеют базис в виде математики. В то время как шаблоны проектирования это просто наблюдения.

Пример бы...
Абстрактно об этом проговорили, скорее всего всё ещё ничего не понятно. Давайте приведём примеры алгебраических структур, так будет лучше. Все же слышали эти модные словечки "функтор", "моноид" и прочее? Так вот это и есть те самые алгебраические структуры.

Если смотреть на алгебраическую структуру "Functor", то она нам говорит, что должна быть операция - map(). Это операция в качестве аргумента получает функцию, которая берёт что-то типа А и возвращает что-то типа B. На примере TypeScript-а это выглядело бы как-то так:

interface Functor<A> {
map<B>(fn: (a: A) => B): Functor<B>;
}


И если наш объект реализует данный свод правил (интерфейс), то мы можем сказать, что наш объект является функтором. Но не одним же функтором живём.

Их много
Алгебраических структур на самом деле много: функторы, апликативы, моноиды, профункторы и так далее. Это всё алгебраические структуры - свод правил, описывающий возможные операции над элементами.

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

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

Читать полностью…

Wild Wild Web

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

Читать полностью…

Wild Wild Web

Языки программирования и спецификации

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

Наверняка многие из вас слышали о таком понятии как "формальная спецификация языка программирования". Что же это значит?
Спецификация языка программирования это документ, в котором описано как язык должен себя вести. Как он должен выглядеть с синтаксической точки зрения. Какие семантические свойства даются тем или иным конструкциям языка. И так далее... Другими словами, спецификация это формальный контракт между пользователями языка и авторами компиляторов, которые реализуют этот язык.

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

Приведу примеры языков программирования, у которых есть спецификации:

- JavaScript
- PHP
- Java
- C++

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

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

На сегодняшний день, языки и их спецификации принято развивать параллельно.
Развитие спецификации формализует поведение компилятора, а развитие компилятора проверяет что спецификация реализуемая.

И довольно много языков начинались вовсе без спецификации и только спустя определенное время она у них появлялась (тот же PHP, Python, даже у TypeScript была попытка). При этом, некоторые языки до сих пор живут без спецификации и им не мешает это ничуть. Например, Rust и Typescript.

Чем аргументируется отсутствие спецификации для языка программирования?
В случае, если язык не планируется быть встраиваемым и при этом у этого языка только один компилятор - чаще всего обходятся так называемой референтной реализацией.

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

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

Таким образом у языков без спецификации есть "авторитарная" реализация и набор тестов, проверяющий, что компилятор ведёт себя соответствующе.

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

Читать полностью…

Wild Wild Web

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>,
]


Мои поздравления! Мы получили простейший "синтаксический анализатор" с использованием системы типов.

Читать полностью…

Wild Wild Web

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

Читать полностью…

Wild Wild Web

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. Я не знаю, когда это собираются уйти в релиз, но предположу что очень скоро, учитывая что оно уже в основной ветке.

Читать полностью…

Wild Wild Web

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

Скажем, у вас огромная монорепа. Вы уже поняли что без подхода "One Version" вы просто утонете в менеджменте зависимостей. Локальные билды, чтобы проверить своё изменение в одном пакете, проходят по 30-40 минут. И так далее и тому подобное, рассказывать можно очень долго.

Как только у вас появляются подобные проблемы, скорее всего вам пора задуматься о более серьезном инструменте для управления. Один из них это как раз Rush.

Он предоставляет вам из коробки инкрементальную компиляцию, то есть ничего не будет собираться, если оно не изменялось. Скажем, вы собрали пакет А и пакет Б. Rush построил отпечаток этих пакетов и записал в своё состояние. После, вы пошли и поменяли пакет Б и снова запустили билд. Rush посмотрел, что отпечаток пакета А не изменялся, а значит артефакт будет тот же. Следовательно, билд можно пропустить, сэкономив уйму времени.

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

С его помощью можно сделать настоящие дистрибутивные билды на CI. То есть, одну часть монорепы собирать на одном кластере, а вторую часть монорепы собирать на втором кластере. Для Rush нет необходимости иметь всю монорепу в полном составе. Он так организовывает файловую структуру, что все пакеты являются самодостаточными и их можно собирать отдельно от всей монорепы.

Я уже так много расписал, поэтому остановлюсь и подчеркну следующее. Кривая входа в Rush большая, без шуток. Для большинства из вас он вам не нужен, скорее всего. И вы только дольше будете мучиться с его настройкой, чем с проблемами, которые он решает. Но как только вы поймете, что у вас начались проблемы с монорепой - возможно пора посмотреть на инструменты типа Rush.

Другие инструменты?
Я описал только те, с которыми мне пришлось поработать и я на них не одну монорепу собрал. Уверен, что есть и другие инструменты, например Nx.js. Пишите в комментариях, чем вы собираете свои монорепы или вам повезло работать по "классической" схеме? Какие инструменты есть, может я о каких-то не знаю и есть что-то лучше или хуже?

Дискуссия
И, к слову, раз уже подняли эту тему, то оставлю ссылку на нашу дискуссию во время Framework Days. Там участвовали разные люди с разным опытом и каждому было что сказать по поводу монореп и стоит ли с ними жить.

Читать полностью…

Wild Wild Web

TypeScript Configuration

Недавно я делал пост о моей личной подборке плагинов к ESLint-у и меня попросили сделать похожее о конфигурации TypeScript. Сегодня решил посвятить это утро моей личной подборке флагов к нему.

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

- allowUnreachableCode (false) - бросает ошибки компиляции, если у вас есть код, который никогда не будет выполнен.
- alwaysStrict (true) - всегда добавлять к сгенерированному JS файлу директиву "use strict". Это позволяет движкам выполнять JS в строгом режиме (по умолчанию это не так) и имеет свои преимущества, но это отдельная тема.
- no* (true) - семейство флагов, которое начинается с no это флаги линтера. Тут решайте сами для себя, что включить, а что пропустить, но большинство флагов очень полезные. Например, noImplicitAnynoImplicitThis и подобные я настаиваю на включении всегда. Они позволяют избежать неявных операций над типами, как например неявное присваивание типа 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. Если у вас есть время и желание посмотреть на полный список, то его вы можете найти по этой ссылке.

Читать полностью…

Wild Wild Web

🔄 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). Поэтому тащить её в прод не советую, но поиграться можно.

Читать полностью…

Wild Wild Web

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

Читать полностью…

Wild Wild Web

Откуда идёт вся эта история с типами?

Один лагерь топит за то, что типы не нужны. Вот на 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?

Читать полностью…

Wild Wild Web

У меня как-то спросили, а есть ли смысл держать исходные коды вашего проекта на 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 в него. И так далее...

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

Сегодня у нас "лёгкий" пост, решил ответить на вопрос от подписчика. И вот смотрю сейчас в беклог и вижу ещё один вопрос от подписчика по поводу типов. Где они полезны, где не очень, и так далее - холиварная тема. Возможно, напишу своё мнение на этот счёт. Так как обстоятельства не бывают "бинарными", сценарии применения типизированных языков также не "бинарные". Постараюсь раскрыть эту тему 😉

Читать полностью…

Wild Wild Web

Все мы работаем в приложениях, которые позволяют нам редактировать наш исходный код. Это могут быть просто редакторы текста, те же Vim, Emacs, Notepad++, да что угодно. Так и полноценные, напичканные разными возможностями, интегрированные среди разработки. К IDE можно отнести такие продукты как Visual Studio Code, Intellij, Eclipse, NetBeans и так далее.

Обычно, при работе в IDE, вы получаете автодополнение кода, рефакторинг, навигацию по проекту и прочие вещи, которые так или иначе помогают вам в этой ежедневной рутине. Раньше, такие вещи реализовывались на стороне сред разработки. То есть, если, скажем, IDE X не поддерживает работу с языком программирования Haskell (например), то эта IDE в принципе не могла использоваться для работы с Haskell. Вы, конечно, всё ещё могли редактировать код в ней, но вы бы не получали все эти удобства, описанные выше.

Как ситуация выглядит сейчас (на примере VSCode)? Сейчас, если вы хотите добавить поддержку нового языка программирования к вашей IDE, вам нужно просто поставить расширение к VSCode, которое добавляет эту функцию. Как же так получается, что добавление поддержки нового языка программирования - это просто расширение?

В контексте VSCode, мы обязаны такой спецификации как Language Server Protocol (так же известный как LSP). Language Server Protocol - это спецификация, которая описывает протокол общения между редактором кода (VSCode, например) и языковым сервером.

Языковой сервер - это отдельный процесс, который предоставляет возможности по поддержке языка программирования. Анализирует код программы, даёт всю необходимую информацию о том, что происходит с вашим кодом. И так далее... В общем, языковой сервер - это "мозги" поддержки языка программирования Х. Но у этих "мозгов" нет клиентской части, только серверная. Ключевой момент - эта серверная часть должна реализовывать спецификацию LSP.

Клиентская часть - это просто UI, который по описанной спецификации общается с языковым сервером. Происходит это через JSON-RPC по IPC (Inter-Process Communication).

Давайте теперь представим, что мы открываем проект на TypeScript в VSCode. Начинаем с ним работать и появляется необходимость вызвать автодополнение кода. Что (скорее всего, я не смотрел исходный код VSCode) происходит в этом случае? На эту возможность, есть четко описанная удаленная команда - textDocument/completion. Формируется запрос на автодополнение для строчки кода Х в контексте Y и языковой сервер отвечает - "вот у меня есть такие фрагменты, которые я могу предложить в этом месте". VSCode берёт эту информацию и рисует у себя в UI. В результате, вы получаете автодополнение.

По такому же подходу работают и остальные возможности IDE: автодополнение, навигация по иерархии вызовов, сворачивание блоков кода и очень много другого. На всё это есть удаленные команды, которые реализовывает языковой сервер. VSCode только ходит по IPC с запросами, которые строго следуют спецификации LSP.

Вот так, вкратце, и работает VSCode. На большинство современных языков программирования есть написанные языковые сервера, а VSCode просто интерфейс, который общается с ними по IPC. Вы можете об этом [LSP] почитать подробнее на их официальном ресурсе.

Пишите в комментариях, сталкивались ли вы с LSP при настройке своего рабочего окружения? Это довольно популярная тема и поддерживается даже в Vim (который, на первый взгляд, просто редактор, но нет). Возможно, у вас есть другие вопросы, связанные с этой тематикой, тоже пишите\спрашивайте. Не всегда есть идеи, о чем написать на канале, а так, вы задаете тему обсуждения и новых постов.

Читать полностью…

Wild Wild Web

В прошлом посте я немного затронул тему типов, а именно "Opaque Types". Так почему бы не продолжить её и не рассказать о ещё одной интересной возможности языка - "Tagged Unions" (моя версия перевода - "объединения с дискриминантом").

Все мы знаем, что у TypeScript есть возможность задавать объединения типов. Кстати, тот факт, что мы используем одни типы для создания других типов делает их алгебраическими. Например, мы можем создать тип Color, используя три литеральных типа:

type Color = 'red' | 'yellow' | 'green'

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

И все эти объединения будут работать и всё прекрасно, но что, если бы мы хотели отобразить что-то более сложное? Например, отобразить объекты разной формы, но при этом объединить их под общим признаком.

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

Всё это разные объекты с разной формой:

type LoadingInProgress = {}
type LoadingSuccessull = { data: unknown }
type LoadingErrored = { error: Error }


Имея три возможных состояния, давайте сделаем алгебраический тип Loading, который будет содержать все эти состояния:

type Loading = LoadingInProgress | LoadingSuccessull | LoadingErrored

Теперь, мы можем использовать тип Loading, чтобы отобразить возможность разных состояний. Но! Проблема и неудобство в том, что нам придется вручную сужать типы до нужного нам, что не очень то и хочется. Давайте добавим дискриминант!

Объединения с дискриминантом в TypeScript - это объединения у которых присутствует одно общее поле (с уникальным символом). Оно и будет служить дискриминантом, чтобы TypeScript по нему вывел что же за тип у вас сейчас в руках.

Пусть наш дискриминант будет называться kind и добавим его ко всем состояниям:

type LoadingInProgress = { kind: 'PROGRESS' }
type LoadingSuccessull = { kind: 'SUCCESS', data: unknown }
type LoadingErrored = { kind: 'ERROR', error: Error }


Так как в этих состояниях присутствует одно общее поле kind и оно уникальное, то можно легко по наличию этого поля понять, а какой именно тип из объединения у нас сейчас находится. Что и делает TypeScript.

Теперь, вы можете указывать тип вашего объекта, как Loading, и получить неявный вывод нужного типа по дискриминанту:

const progress: Loading = { kind: 'PROGRESS' }
const success: Loading = { kind: 'SUCCESS', data: {} }
const error: Loading = { kind: 'ERROR', error: new Error('Something went wrong, 500!') }


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

Мы очень часто применяем объединения с дискриминантом в тех местах, где у нас могут быть разные объекты, разной формы, но при этом у всех общий признак. Очень удобная штука, которая помогает типизировать состояния и даже(!) (в некоторых ситуациях) реализовать некое подобие pattern-matching.

Пишите в комментариях, в каких ситуациях вы используете объединения с дискриминантом и часто ли они вас спасали? Если не спасали, то почему?

Читать полностью…
Subscribe to a channel