Майк Башуров

Майк Башуров

Темы
Неделя
Jan 27, 2020 → Feb 2, 2020

Архив недели @saitonakamura

Понедельник


Йоу! Под конец января с вами Майк Башуров @saitonakamura. Много лет делал .NET, ушел во фронтенд и не жалею. Будем много говорить про TypeScript, немного про Flow и про другие языки, обсудим около-архитектурные вещи, фриланс-номадство, ...

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

Начнем с начала тайпскрипта, то бишь с его прошлого Появился он еще в 2012 году zdnet.com/article/micros… , релиз 1.0 состоялся в 2014 devblogs.microsoft.com/typescript/ann…

В то время язык был местами ближе к C# чем к JS, отсюда namespaces, classes, private/public/protected. В целом это неудивительно ибо у истоков стоял (и стоит до сих пор) Андерс Хейлсберг, создатель Turbo Pascal, архитектор Delphi и, сюрприз C#

Итак на дворе 2012, Путин возвращется на президенское кресло после Медведева, до ES2015 еще три года (я кстати сначала загуглил в каком году он вышел), от ES5 уже прошло три года. И вот он, новый язык компилируемый в JS с прикольными фичами и со статической типизацией!

Несмотря на то что была взята часть фич шарпа, другая часть была переосмысленна Типизация опциональна и имеет safehatches: any, Object, Function и т.п. Синтаксис типов менее инвазивный, в отличии от c-style (int i) используется var i: number

Несмотря на то что в языке сразу были namespaces, назывались они в начале modules. Естественно это вызвало name clashing когда в ES2015 поэтому было решено переименовать их в неймспейсы

Вообще на релизе в ТС не было очень многого чем мы с вами пользуемся сейчас. Даже если не считать фичи ES2015 до TS 2.0 (а это было в 2016) появились: protected, unions, intersections, type, type guards, abstract classes и еще много всего

Но 2.0 превнес наиважнейшую фичу: nullable types. Возможность сказать: "Вот здесь будет точно string/number/какой-то объект, но точно не null и не undefined, а вот здесь может быть и null" это способ исправить billion dollar mistake infoq.com/presentations/…

Многие современные языки являются функциональными языками с монадой Maybe (Haskell, Scala, F#, OCaml, ReasonML) имеют эту фичу изначально (Kotlin) завозят эту фичу себе (C#, TS) планируют завезти (Dart)

Если кому интересно, работаю я в WiseBits, Лимассол, Кипр (недавно переехал), буду тут делать комьюнити, а также я один из мемберов программного комитета @HolyJSconf

Про монадки и прочее фп мы поговорим попозже, а пока вернемся к истории Вообще, если хочется тщательно проследить историю развития ТС, то стоит просто почитать ченджлоги typescriptlang.org/docs/handbook/… Я же просто поделюсь своими впечатлениями и мыслями по поводу фич

Вторник


Извиняюсь за перерыв, работать тоже надо (как ни странно) Продолжаем В 2.0 также появилась опция --skipLibCheck. Для тех кто печется о том чтобы не было ошибок при билде (а так делать стоит) можно отключить ошибки в чужих тайпингах

Варианты действий при ошибке компиляции в тайпингах Идеальный: форкнуть существующие тайпинги, поправить ошибку, сделать PR, пока он не влит, поменять пакет на пропатченный Нормальный: откатить пакет до версии где нет ошибки И, наконец, приемлемый: использовать skipLibCheck

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

@jsunderhood @musuk Можно ещё прочитать twitter.com/johnny_reilly/…
Отличное чтиво! twitter.com/timocov_/statu…

В 2.1 появился исключительно полезный типооператор keyof. На нем строится наверное процентов 80 (люди склонны верить рандомным цифрам, да?) преобразований типов и он дает клевый intellisense. Скажем есть вас компонент с иконками и object-map для svg
notion image

И вы хотите его типизировать, но не просто icon: string, а чтобы можно было только plus и menu Конечно можно написать ручками
notion image

Но можно просто написать keyof typeof svg и он автоматом возьмет все свойства svg! Самое полезное здесь конечно то что при обновлении svg вам не придется обновлять еще и тип
notion image

Подробнее про keyof здесь typescriptlang.org/docs/handbook/… Из минусов (на самом деле скорее особенностей): keyof работает только на самом верхнем уровне То есть вы не сможете описать что-то вроде const obj = { field1: { field2: { field3: 'str' } } } _.get(obj, 'field1.field2.field3')

Наверное стоит еще остановиться на typeof раз уж я его упомянул Дело в том что для ТС при парсинге и компиляции существуют два мира: мир рантаймовых конструкций и мир конструкций типов (который в рантайме не будет)

Так как типов в рантайме нет (об этом мы с вами еще поговорим), то влиять на рантайм как бы нельзя Но вот с помощью объектов/функций можно спокойно влиять на типы! И чтобы перейти об объекта/функции/etc. к его типу нужно просто написать const foo = () => {} type A = typeof foo

Таким образом const svg = { plus: './plus.svg', menu: './menu.svg } type Svg = typeof svg // { plus: string; menu: string } type SvgKey = keyof Svg // или keyof typeof Svg // 'plus' | 'menu'

Лирическое отступление лирического отступления: Особо пытливые заметили что typeof является правоассоциативным оператором, то есть сначала выполняется typeof Svg и только потом keyof от результата Это я просто так выделываюсь перед компиляторщиками чтобы они подумали что я свой

Возвращаемся на землю (нет, не плоскую) Еще в 2.1 появились mapped types. Это вторая основа тайплевела который можно творить на ТС и об этом тоже будем потом (сори)

И наконец опция с путающим названием --alwaysStrict Она непохожа на остальные опции семейства --strict (strictNullableChecks, strictFunctionTypes etc.) ибо она про то что код парсится в strict-режиме и в каждый скомпилированный файлик кладется "use strict" директива

В 2.3 появилась важная опция --strict Она включают кучу других опций которые делают ваши типы строже и правильней typescriptlang.org/docs/handbook/… Если честно, мне кажется все проекты на ТС должны стремится к тому чтобы компилироваться со strict: true

Следующим экспонатом (мы видимо в историческом музее) версии 2.3 будет --checkJs Совместно с JSDoc это то на чем строится миграция большинства мало-мальски больших (pun intended) JS проектов на ТС (опять же попозже, вот я разрушитель надежд, да?)

В 2.4 завезли string enums enum Colors { Red = "RED", Green = "GREEN", Blue = "BLUE", } Несмотря на их кажущуюся пользу я считаю их вредными и буду подробнее об этом говорить в секции "Как Я такой-растакой буду указывать как ВЫ должны использовать ТС" (шучу)

В 2.6 добавили страшно желаемую фичу // [@ts](https://twitter.com/ts)-ignore Если где-то есть ошибка и вы хотите заткнуть компилятор, но использовать as any или что-то подобное не хотите/не помогает, то вот ваш выход Какие были кейсы для [@ts](https://twitter.com/ts)-ignore у вас?

@jsunderhood Мы наоборот перешли на них, т.к. важность порядка при синке с бэками — неудобно
Вполне валидный кейс twitter.com/rm_baad/status…

@jsunderhood @boriscoder @musuk В больших и сложных проектах такие мелочи могут очень больно ударить, ловили не раз.
Я вообще буду рад если вы (аудитория) будете делиться болью Обсудим, может даже что-то порешаем, на крайняк посидим поплачем вместе twitter.com/neruchev/statu…

Среда


На сегодня наверное все, завтра добьем историю и поддадим хардкору

@jsunderhood Вот это кстати очень "боль". Имею в виду "обьяснить команде". Тяжело это. Большинство, чаще всего противятся изменениям, особенно если думать надопо новому. И очень мало людей умеют аргументировать свою позицию обьектвино, измеримо, а не "мне так больше нравится".
Поддерживаю Я не виню в этом людей, но коммуникация сложная даже если ты не пытаешься что-то продать twitter.com/Oleg75113370/s…

Я как раз неделю назад занимался тем что продавал всякую продвинутую типизацию (сам ТС команде уже продали до меня к счастью) Думаю стоит поделиться этим опытом

И в самом деле, впиливать в уже существуюший проект (особенно большой) гораздо сложнее Но у меня больше проблем не технического, а коммуникационного и психологического характера t.co/lRyN1oK4WI

@jsunderhood @ts Типы сложные, а релизить надо вчера :D
Да, несмотря на то что конечно надо стремиться к отсутствию авралов и СРОЧНА НАДА, без этого никуда twitter.com/blvdmitry/stat…

Дело в чем. Раньше когда вы не знали что будет за тип, был один выход: any any плох тем что с ним можно делать что угодно и он как рак распространяется по вашему коду Но что если вы РЕАЛЬНО не знаете что за тип приходит И вы хотите сначала отмерить, и только потом отрезать

А с unknown вы сначала должны доказать компилятору что он не пустой, в нем есть такое поле и оно такого-то типа и так далее И только потом использовать

Для того чтобы доказать используются type refinements (они же type guards) Выглядеть может примерно так Можете поиграться сами typescriptlang.org/play/#code/MYe…
notion image

Но если вот такие type guards работают не очень, то что делать? Ответ: user-defined type guards Поиграться typescriptlang.org/play/#code/C4T…
notion image

Проблемы с ними две Как видите надо много писать Имплементация лежит исключительно на вас То есть можно написать const isMyType = (obj: unknown): obj is MyType => true и ТС усом не поведет

@jsunderhood Боль - unsoundness, ограниченный вывод типов по call site - много печатать тайпингов. Но это все by design, так что только плакать
Да, полностью согласен Но у меня есть туз в рукаве twitter.com/somerandstring…

Тем не менее само наличие unknown уже заставляет задуматься об избыточном использовании any, что не может не влиять на сознательность решений То как в реальности стоит делать рантайм проверки обсудим попозже (да, там будет io-ts, но далеко не он один)

@timocov_ Да, я где-то видел как при breaking changes кор контрибьюторы писали codemodes для definitely typed и прогоняли на всем репозитории К тому же есть возможность поставлять разные тайпинги для разных версий ТС
В 3.1 как раз появилась возможность указывать разные версии тайпингов для разных версий компилятора typescriptlang.org/docs/handbook/… twitter.com/jsunderhood/st…

В 3.4 появилась наикрутейшая возможность const assertions, например Раньше когда у вас была функция которая возвращала объектный литерал у вас не было возможности как-то указать что этот литерал планирует быть иммутабельным

Характерный пример это action creators в редаксе const createLoad = () => ({ type: 'LOAD' }) type LoadAction = ReturnType<typeof createLoad> // { type: string }

Ну казалось бы стринг и стринг, что такого то А такого то, что еще есть такая штука как discriminated unions которая позволяет делать две вещи 1 Различать различные типы по полю (различные экшены по type) 2 Проверять что все возможные экшены обработаны basarat.gitbook.io/typescript/typ…

И для того чтобы они (discriminated unions) работали надо чтобы type был string literal, а не просто string То есть надо type LoadAction = { type: 'LOAD' } Так вот с const expressions вы можете написать as const и type widening-а не произойдет

type widening это когда в результате некоторых манипуляций тип становиться менее строгим вот мы возращаем в нашем экшене объектный литерал { type: 'LOAD' } Но ТС справедливо считает что мы можем сделать вот так const loadAction = createLoadAction() loadAction.type = 'HECK'

Поэтому ему надо подсказать что литерал будет иммутабельный const createLoadAction = () => ({ type: 'LOAD' } as const) type LoadAction = ReturnType<typeof createLoadAction> // { type: 'LOAD' } УРА

as const работает не только на объектных литералах но и на обычных то есть можно написать const createLoadAction = () => ({ type: 'LOAD' as const }) и эффект будет похожим за тем исключением что остальные поля объекта останутся мутабельными

@jsunderhood Как ты считаешь, чем лучше транспилировать ts? TSC Babel Сначала TSC (из TS в ES2015), затем babel (из ES2015 в ES5)
Траспилить бабелем, тайпчек отдельно Несмотря на то что официально майки рекомендуют траспилить через tsc, бабель слишком хорош чтобы игнорировать его: babel-preset-env, babel-macros и другие интересные плагины 3-ий вариант просто вызовет проблемы со скоростью и сорсмапами twitter.com/ifelseapps/sta…

В 3.5 наконец-то добавили хелпер Omit в стандартную поставку В 3.6 нормально затипизировали генераторы typescriptlang.org/docs/handbook/…

В 3.7 в срочном порядке добавили поддержку nullish coalescing и optional chaining (причем прямо с 3-его стейджа ЕМНИП, рисковые парни) И assertion functions type-guards и custom type-guards у нас уже были, но они возвращали boolean Соотвественно надо было обкладываться if-ами

Assertion function это история про exception flow control То есть проверили предусловия контракта и пошли дальше писать код без вложенности typescriptlang.org/docs/handbook/…

Еще в 3.6 пофиксили некоторые рекурсивные типы typescriptlang.org/docs/handbook/… Раньше можно было делать рекурсивные типы когда исходный тип не участвовал в рекурсивном юнионе напрямую Нихера на словах непонятно youtube.com/watch?v=LAAhV3…

Короче вот так было можно type Tree<T> = { value: T, children: Tree<T>[] } а вот так вот нельзя type Json = | string | number | boolean | null | Record<any, Json> | Json[] Начиная с 3.6 можно и вот так ^

Кстати до 3.6 был интересный хак чтобы обходить такое Дело в том что по стечению обстоятельств type вычисляется eager, а вот interface - lazy Это позволяло оборвать бесконечную рекурсию вот таким способом:

type Json = | string | number | boolean | null | JsonObject | JsonArray; interface JsonObject { [property: string]: Json; } interface JsonArray extends Array<Json> {}

If you’re wondering a bit about what the use-case for import type is used for in @typescript 3.8 - one of it’s main use cases will make @babeljs’ life a lot easier typescriptlang.org/v2/en/play?ts=…
В 3.8 нас главным образом ожидают очередные новые фичи ECMAScript (например top-level await), но самая мякотка в type-only imports twitter.com/dskr_dev/statu… Если интересно чем это хорошо - почитайте вот этот тредик twitter.com/orta/status/12…

Ну вот и все про историю! Пойду поработаю, следующий тред будет про то как я вижу идеальный проект на ТС

🔥Тред (Майк Башуров)
@jsunderhood Для себя в последнее время открыл ttsc - кастомная обертка, которая поддерживает плагины. Позволяет избавиться от бабела в проекте во многих случаях
Про ttsc, кастомные траснформеры я буду говорить очень подробно, но сейчас между делом упомяну что 1 В целом tsc поддерживает плагины, но только через compiler api. ttsc просто делает обертку которая умеет в аналог babelrc 2 Экосистема плагинов несравнима конечно twitter.com/blvdmitry/stat…

@jsunderhood Самая большая боль пока — это вывод ошибок в сложных типах
Дико поддержу Кто-то где-то сказал что у статической типизации ужасный UX И это правда так, типы обычно не говорят где вы совершили ошибку, они говорят что что-то не так и что конкретно не так У меня однако есть одна дикая идея как эту ситуацию можно улучшить twitter.com/rm_baad/status…

Следующий тред про то как я вижу идеальный проект на ТС (и почему, естественно) Как будто нам дали требования и мы стартуем новый Про енамы, декораторы, branded types, discriminated unions, инварианты и другое Поехали

Первым делом установили npm i -D typescript И инициализировали конфигурацию npx tsc --init Дефолты на данный момент крайне адекватные, там включен esModuleInterop, strict и всякое другое

На esModuleInterop остановимся поподробнее Опция появилась в 2.7 и помогла сделать поведение импортов консистентым с бабелем Раньше когда нужно было импортнуть содержимое module.exports из commonjs модуля в ТС необходимо было писать import * as React from 'react'

Против import React from 'react' у бабеля Реальность показала что прав был бабель: * as React это namespace import по спеке и он не Callable но в commonjs модуле может быть написано module.exports = function foo() {} опа, нарушили стандарт

Кто хочет подробно разобраться в теме: я в свое время накатал объемную статью (осторожно, Инглиш) itnext.io/great-import-s… почитайте, если что-то непонятно, задавайте свои ответы

Окей, с модулями разобрались, все стрикты врубили, что дальше Дальше стоит определиться с тем какие фичи языка использовать Мой rule of a thumb: писать будто бы современном ECMAScript + типы Более того, именно в эту сторону сейчас движется ТС

Почти все фичи которых нет хотя бы на stage 4 были введены довольно давно Большинство новых фич не относящихся с ECMAScript спеке влияют исключительно на типы Какие в итоге фичи нестандартные? namespaces, enums, decorators и reflect metadata

В 2.4 завезли string enums enum Colors { Red = "RED", Green = "GREEN", Blue = "BLUE", } Несмотря на их кажущуюся пользу я считаю их вредными и буду подробнее об этом говорить в секции "Как Я такой-растакой буду указывать как ВЫ должны использовать ТС" (шучу)
namespaces уже совсем мало кому нужны, не вижу смысла на них останавливаться стоит однако заметить что в тайпингах (.d.ts) они все еще очень полезные, но тайпинги это вообще другой мир следующие на очереди енамы twitter.com/jsunderhood/st… итак, в чем с ними проблема

Начнем с того что енамов в ТС 4 вида enum, const enum, declare enum и declare const enum Все они имеют некоторые особенности и отличаются друг от друга (некоторые пишут в рантайм, некоторые не умеет траспилить бабель и т.д.) Те что пишут в райнтайм, пишут туда страшные вещи
notion image

Извиняюсь, 6 там не к месту (но и без нее непросто) В третьих помните я говорил чтобы получить тип от рантаймового значения надо сделать typeof Так вот енам, он особенный, ему не надо typeof, он сразу одновременно и тип и объект!

Стоит ли также упомянуть что enum врядли станет когда-нибудь частью стандарта? И при всем при этом абсолютное большинство кейсов енама покрывается двумя вещами: объектами и string literal union

Если вам нужен объект с заранее известными полями в которых будут хранится строковые значения, возьмите объект! enum Colors { Red: 'red', Blue: 'blue' } прекрасно транслируется в const Colors = { Red: 'red', Blue: 'blue' }

если же у вас есть какое-то апи завязанное на строки, к примеру enum RequestType { GET: 'GET' POST: 'POST' } const fetch = (type: RequestType) => ... Вместо енама вы можете использовать юнион type RequestType = 'GET' | 'POST' const fetch = (type: RequestType) => ...

С вышеназванными кейсами видимо понятно, так когда же енам все еще полезен? 1 Чтобы 1в1 скопировать енамы с бэка и не париться. То есть у вас есть скажем джава и шарп на бека и они используют енам у себя и в пейлоаде апи Вы просто копируете в ТС и вуаля, синхронизировались

2 Для придавания семантики числовым опциям. Этим опять же может грешить бэк и принимать вместо строковых ключей - числовые (которые потом завязываются на их енам) Если в случае со строкой fetch('GET') читается прекрасно, то fetch(0) - not so much enum RequestType { GET:0, }

Четверг


Ладно, енамы, что еще? Декораторы! С ними... Сложно. Мне как фича языка они нравятся, НО Есть ощущение (может кто-то пруфанет) что декораторы (и reflect metadata) завезли ради Angular и просто надеялись что потом и в спеку войдет Чуда как всегда не случилось

И декораторы уже кажется два раза падали с stage 3 на stage 2 с ломающими изменениями В итоге состояние дектораторов в ТС и ныне там и видимо они ждут когда наконец-то можно будет имплементировать новую версию Но страдают как всегда пользователи

Во-первых декораторы не могут влиять на типы декорируемой сущности поэтому написать @connect(mapStateToProps) const Foo: React.FC<{}> = props => ... не выйдет Точнее написать-то выйдет, но вот убрать StateProps из пропсов не получится

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

@jsunderhood Прям как класс!
Нуу кстати, да, это я зафейлил, сорян twitter.com/dskr_dev/statu…

Но если уж инвестируете в декораторы (например Nest) то еще есть reflect-metadata Это вот уж точно чисто для ангуляра штукенция чтобы декоратор имел доступ не только к дескриптору элемента, но и к его типу Для этого тип записывается в рантайм

Несмотря на то что фича довольно крутая, меня очень смущает то что ее никто кроме Анга не использует Но судя по всему подвижки имеются, вот например @rm_baad будет на @HolyJSconf рассказывать про то как они это юзают (как я нативочку то ввернул, а?)

Со старыми фичами разобрались, про остальное продолжим завтра Stay tuned

@jsunderhood Декораторы ни разу не поднимались на stage 3
Меня поправляют twitter.com/chicoxyzzy/sta…

@jsunderhood Ещё inversify есть, typeorm, typegoose и прочая куча пакетов для ORM и IoC.
Оказалось что на самом деле reflect-metadata использует много кто! Я думал что всякие inversify живут на строковых ключах twitter.com/MeFCorvi/statu…

I'm back in black! Давайте еще посмотрим на опции компилятора Стоит включить --forceConsistentCasingInFileNames: дело в том, что на Windows файловая система case-insensitive, а на юниксе нет Это может привести к интересным багам которые еще и неприятно фиксить

То есть если кто-то с винды напишет import Component from './mYComPoNenT' это соберется у него и упадет на CI (если он не винда) и всех остальных Но проверить на винде правильный кейсинг нельзя, фс не позволяет! Для этого придумали вот такой костыль:

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

Да, вот еще то что ключа 4 тоже может вызвать проблемы потому что это контринтуитивная фигня twitter.com/cakeinkatya/st…

module выставляем в ESNext: собирать наверняка будем каким-нибудь вебпаком или роллапом и это позволит использовать чанки через динамический импорт и трешейкинг moduleResolution стоит явно выставить в "node", это решает некоторые странные проблемы

От компилятора переходим к организации проекта. Сразу советую создать в корне папку typings и указать ее в typeRoots "typeRoots": ["./typings", "./node_modules/@types"] если вам понадобиться дополнить (или создать) тайпинги то просто создайте typings/some-library.d.ts

И там уже declare module "some-library" { // blabla }

Также я считаю всевозможные опции типа webpack alias, tsconfig.paths etc. вредными Почему? Это нестандартная фича и для любого инструмента который работает с вашим проектом вам придется дублировать это Вот сделали вы webpack alias, пишете в ТС import Obj from 'myRoot'

А ТС вам и говорит, не знаю что за myRoot, он же не знает ничего про вебпак Хорошо, добавили в tsconfig.paths, ТС успокоился. Запускаем тесты и получаем ошибку резолва модуля. Фиксим ее, падает сторибук. Аккуратно наследуем конфиг сторибука от вашего вебпака, ну вы поняли

Вы наверняка спросите, а как тогда быть c import Something from '../../../../../../this/is/root/then/its/this/folder' Я считаю нужно уплощать структуру. Код и его зависимости это граф, и попытки выразить их через дерево (файловая система) обречены на провал

Про использование symbolic links в кодовой базе даже говорить не хочется Поэтому файловую систему нужно использовать для группировки высокоцелостных (high cohesive) кусков кода вместе Вот здесь парень хорошо объясняет youtu.be/xBa0_b-5XDw

Кратко, вместо логической группировки actions/notifications.ts actions/chat.ts reducers/notifications.ts reducers/chat.ts делаем группировку по функционалу notifications/reducer.ts notifications/actions.ts chat/reducer.ts chat/actions.ts Нейминг выбирайте сами

В редаксе это называется ducks, детали могут варьироваться, но я думаю общая мысль понятна freecodecamp.org/news/scaling-y…

Rule of a thumb: не создавать папку без надобности, в случае необходимости стараться группировать по функционалу Это серьезно сократит вложенность

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

@jsunderhood ты вроде шаришь, расскажи плз как без боли (диких хелперов) сделать Conditional optional type или вроде того? Например тип {a?: string; b?: string;} а я хочу чтобы если оба не переданы то была бы ошибка, а если хотя бы один то нет. Через юнионы не робит или я туплю
Это распостраненная ошибка - делать перегрузки через conditional, обычно это заканчивается болью Перегрузки можно делать двумя способами twitter.com/ch_ronik/statu…

1 Стандартный встроенный способ typescriptlang.org/docs/handbook/… function foo(arg: { a: string }) function foo(arg: { b: string }) function foo(arg: { a: string, b: string }) function foo({ a, b }: { a?: string, b?: string }) { // impl }

2 То же самое можно выразить через юнион инвариантов type Arg = { a: string } | { b: string } | { a: string, b: string } function foo(arg: Arg) {} Но это не позволяет менять return type в зависимости от перегрузки (хотя можно сделать юнион функций)

Пятница


@ch_ronik @jsunderhood ну конкретно для "непустого объекта" - Input extends {[key: never]: any} ? never : Optional<Type>
Сева врывается с хардкорчиком twitter.com/jabher/status/…

Вчера вечером забухал (шучу, были дела) поэтому продолжаем с утреца

Где код писать понятно, как код писать понятно, но какой код писать?? Давайте поглядим на всякие штуки которые в теории должны помочь вам с поддержкой, корректностью и читаемостью

Начнем с инвариантов. Если ко/контра вариантность может показаться сложной для понимания, то в инвариантах сложное только название Инварианты это возможные и взаимоисключающие состояния объекта

Пример: сколько возможных состояний у типа? type Data = { data?: string loading?: boolean error?: Error } Подсказка: нас не интересуют разные значения string или Error, только их наличие

@jsunderhood Идеальный проект остаётся идеальным не так уж долго 🙄
Все так, но нигилизм здесь не слишком полезный. Как говорится жизнь говно, но мы с лопатой! twitter.com/vgermaniu/stat…

Но сколько состояний в реальности? Вероятно 4 | { loading: false } | { loading: true } | { data: string, loading: false } | { error: Error, loading: false } Это и есть нужные нам инварианты и стандартная фп мантра про Making impossible states impossible youtube.com/watch?v=IcgmSR…

На таком маленьком примере не очень понятно в чем профит, но такой подход зачастую отлично ложиться на разные доменные сущности Скажем у нас есть несколько категорий юзеров, например у золотого юзера есть поле которое говорит о том когда закончится его подписка

Опять же если выражать это через опциональные поля выйдет как-то так { username: string role: 'regular' | 'gold' subscriptionEnd?: Date } Потом нам надо в каком-то месте для золотого юзера отобразить время окончания подписки

Пишем const renderSubscriptionEnd = (date: Date) => {...} if (user.role === 'gold') renderSubscriptionEnd(user.subscriptionEnd) И получаем ошибку что subscriptionEnd can be undefined

Окей, давайте проверим const renderSubscriptionEnd = (date: Date) => {...} if (user.role === 'gold' && user.subscriptionEnd) renderSubscriptionEnd(user.subscriptionEnd) И это работает, но мы то знаем что у всех золотых юзеров это поле непустое, так зачем писать этот код?

Давайте объясним это компилятору! type BaseUser = { username: string } type RegularUser = BaseUser & { role: 'regular' } type GoldUser = BaseUser & { role: 'gold' subscriptionEnd: Date }

И теперь тот же самый код работает! const renderSubscriptionEnd = (date: Date) => {...} if (user.role === 'gold') renderSubscriptionEnd(user.subscriptionEnd)

Потому что раньше было 4 варианта юзера { role: 'regular' } { role: 'regular', subscriptionEnd: new Date() } { role: 'gold' } { role: 'gold', subscriptionEnd: new Date() } А мы превратили их в 2 { role: 'regular' } { role: 'gold', subscriptionEnd: new Date() }

И компилятору легче просечь что вы имели ввиду Этим мы получили тот самый discriminated union о котором я говорил ранее. Он же variant, он же тип-сумма ТС теперь знает что если role === 'gold' то там еще будет дополнительное поле с датой

А знаете что еще классно на это ложится? Экшены в редаксе! Так как type это у нас string literal (к тому же с помощью as const мы научились делать типы экшенов минимальными усилиями) то экшены можно дискриминировать (ПРОСТО ОТЛИЧАТЬ, ТУТ НЕТ ДИСКРИМИНАЦИИ, ВСЕ ПОД КОНТРОЛЕМ)

Также можно делать exhaustive checking. Это следующая фигня, если мы выразим те экшены которые обрабатывает редьюсер в виде discriminated union мы сможем проверить все ли экшены обработаны Код максимально упрощен:

type LoadAction = { type: 'LOAD' } type SuccessAction = { type: 'SUCCESS' } const reducer = (state, action: LoadAction | SuccessAction) => { switch (action.type) { case 'LOAD': return case 'SUCCESS': return } const _exhaustiveCheck: never = action }

Работает это так, что ТС зная что приходят два вот таких экшена (которые он умеет отличать друг-от-друга, hence the 'discriminated') и в обоих случаях мы выходим, значит то что после свитча быть не может! В итоге в экшене не лоад и не саксес, а значит там ничего never

never обладает следующими свойствами never is assignable to anything nothing except never is assignable to never То есть его можно присвоить чему угодно. В принципе логично, этой ситуации же как бы в рантайме возникнуть не должно Но ему ничего кроме never присвоить нельзя

Соответсвенно когда мы забудем обработать какой-то экшен то ТС будет ругаться что этот экшн not assignable to never. На моей практике это позволило поймать интересный баг: Был подобный редьюсер который обрабатывал экшены и был примерно такой код

case 'ACTION': return { ...state, blaba } При слиянии потеряли 'return {' и потом вернули '{', а return забыли. В итоге остался неприкаянный object literal который ничего не делал, но код был валидный Когда прикрутили exhaustive check нашли это (экшн был редкий)

Теперь о номинальных типах. Номинальная совместимость (по имени) противопоставляется структурной совместимости (по структуре) То есть в ТС это нормально type A = { field: string } type B = { field: string } const foo = (obj: B) => {} const a: A = { field: '' } foo(a)

А например в C# нет class A { public field: string } class B { public field: string } void Foo(obj: B) {} Foo(new A()) // Ошибка

Так вот, о номинальных типах. Во-первых в ТС их нативно нет, приходится костылить Во-вторых, собственно, нафига? Самый распространенный случай в моей практике это номинальные примитивы. Примитивы это boolean, string, number. Бывает так что тип примитива один, но семантика разная

Например id у разных сущностей. Представим что у нас есть компонент который принимает id юзера и грузит его карточку. Как защититься от того чтобы не передать туда любой другой number? Баг (встречал такое) userId={userSubscription.id} Надо userId={userSubscription.userId}

На помощь приходят брендированные типы type UserId = number & { readonly __brand: unique symbol } declare function foo(userId: UserId): void type User = { id: UserId } const user: User = { id: 1 as UserId } foo(user.id) foo(1) //ошибка typescriptlang.org/play/#code/C4T…

Таким же образом можно брендировать и объекты, чтобы защититься от похожего по структуре, но неподходящего объекта Если интересно, то вот это и другое есть в этом докладе youtu.be/m0uRxCCno00 Еще там был доклад Сергея Черепанова, но я не могу найти его в открытом доступу

И напоследок немного хардкора: opaque types Opaque (непрозрачный) type это такой тип который Номинальный Умеет скрывать свою имплементацию Нужен он нам для того чтобы делать явное управление состоянием по ФП-шному при этом имея инкапсуляцию

Их опять же в ТС нет поэтому будем костылить. Кода там сильно больше поэтому вот плейграунд (он почему-то показывает ошибки местами, но у меня локально на 3.7.3 работает норм) typescriptlang.org/play/#code/PTA…

В чем прелесть? Прелесть в том что так мы инкапсулируем внутреннюю структуру нашего игрового поля (как в ООП), но при этом не получаем проблемы размазанного состояния (как в ООП) управляя им извне (как в ФП) При этом создать Game иначе кроме как через методы модуля game нельзя

В моем примере внешний пользователь вообще ничего не знает про Game, но естественно можно искапсулировать по частям type UserInternal = { id: number; username: string } export type User = Pick<UserInternal, 'username'> & { readonly __brand: unique symbol };

Да, писать довольно муторно и неудобно, но это просто потому что нет нативной поддержки Во flow это opaque (через который можно делать обычные номинальные типы) flow.org/en/docs/types/… В OCaml/ReasonML это делается через module

Вот такие у меня мысли по поводу того как писать на ТС Следующий тред будет про Reason и немного про другие языки

🔥Тред (Майк Башуров)
@jsunderhood Ухх эти типы! Я вот до сих пор не понимаю в чем отличие interface и type в последних версиях TS 🤯 В одном проекте интерфейсы, в другом все чисто на type... Когда какой использовать то?! 🤪
Точно, забыл, очень популярный вопрос Стоит использовать type & (intersection) можно выразить через extends, а вот | (union) никак не выразить Таким образом от interface к type перейти можно, а обратно нет Все mapped types, conditional types это тоже type twitter.com/orlov_vo/statu…

У interface корявенький синтаксис для того случая когда интерфейс - функция interface Foo { (arg: Arg): Return } type это 4 символа, а interface - целых 9 В целом все, исключения поинтереснее

Кстати до 3.6 был интересный хак чтобы обходить такое Дело в том что по стечению обстоятельств type вычисляется eager, а вот interface - lazy Это позволяло оборвать бесконечную рекурсию вот таким способом:
interface нужно использовать когда: Вы пишете библиотеку: ибо interface поддерживает declaration merging, что позволит пользователям вашей библиотеки расширить или исправить тайпинги извне Надо хакнуть неленивое вычисление рекурсивных типов twitter.com/jsunderhood/st…

@jsunderhood Интерфейс выгоден, когда ты создатель либы и хочешь дать возможность людям играться с твоими типами. Пример: styled-components, у которых описан интерфейс для DefaultTheme (пустой), но в своём проекте ты можешь переопределить его, получая нужный тип своей темы.
Даже не то что выгоден, по возможности обязателен Спасибо за бдительность однако twitter.com/_ishmidt/statu…

Суббота


Теперь повысим градус типизации и ФП почти до финала: поговорим о моей тайной (на самом деле не очень) любви: ReasonML Но перед этим немного вспомним flow (for a reason).

Во-первых во флоу есть полноценный вывод типов В ТС весь вывод работает вниз по АСТ (с уточнениями) Это означает что дальнейшие инструкции уже не смогут повлиять на выведенный тип То есть вывести это он может вполне const a = { field: 'str' }

И с возвращаемым типом тоже справится (хотя и не всегда) const foo(a: { field: string }) => a.field

А вот аргументов функции уже нет, потому что объявление функции должно быть до ее вызова const foo = (a) => a foo(1) // тип функции уже выведен как any => any

Так вот флоу это разрулит! Он как-бы решает огромное уравнение где типы это переменные и смотрит сходится все или нет. Если точно не сходится, возникает ошибка компиляции

Таким образом, в теории, можно вообще не писать типы! То есть статические проверки у нас есть, а типы мы не пишем! Isn't that wonderful

Дико поддержу Кто-то где-то сказал что у статической типизации ужасный UX И это правда так, типы обычно не говорят где вы совершили ошибку, они говорят что что-то не так и что конкретно не так У меня однако есть одна дикая идея как эту ситуацию можно улучшить twitter.com/rm_baad/status…
На практике конечно не все так гладко. Я уже говорил что у типизации плохой UX twitter.com/jsunderhood/st… Но используя вывод на полную катушку вы рискуете только это усугубить. Потому что теперь вы не только не понимаете в чем ошибка, но еще и где

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

Все что внутри наоборот типизировать не стоит, ибо это усложняет рефакторинг (явно аннотирование публичного контракта же упрощает)

Есть еще одна интересная особенность систем типов и систем вывода типов: чем более экпрессивна система типов, тем сложнее делать вывод типов Поэтому некоторые системы не гарантируют вывод в 100% случаев (например Haskell)

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

Во флоу есть нативные opaque типы через которые также можно делать номинальные типы flow.org/en/docs/types/…

Во флоу также есть более интересные utility types, например $Call flow.org/en/docs/types/… Кто-то может возразить что в ТС есть ReturnType, но это не совсем одно и то же $Call возвращает результат вызова с определенными аргументами

То есть если функция возвращает разное в разных случаях (читай перегрузки) то флоу высчитает соответствующий тип ТС у перегрузок выводить тип возврата нормально не умеет

Это я все не к тому что ТС плохой и не к холивару, а к тому что стоит всегда интересоваться тем что происходит вне вашего типичного окружения/языка/технологии Ну и конечно чтобы плавно перейти к изюминке: ReasonML

Еще немного истории, был такой язык OCaml сделали в 1996 году. Это был (и есть) мультипарадигменный язык из ML семейства и как-то так случилось что Фейсбук (и не только) стал его активно использовать (у них например есть юникернел целиком на OCaml mirage.io)

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

Потому что им в голову пришла другая идея: а что если писать фронтенд на окамле и компилировать в жс? Идея конечно не нова, был и Elm и PureScript и F# с Fable, как избежать их ошибок? Для этого они решили что язык похожий на язык ML семейства плохо подходит

Слишком чужеродный синтаксис, слишком неприятно мапится на JS. Хорошо, а что если сделать синтаксис гораздо ближе к жс, сделать похожие на жс структуры и сделать удобный и супертонкий (а в основном и вовсе отсутствующий интероп)? И самое главное - НЕ ТЕРЯТЬ ДОВЕРИЯ КОМЬЮНИТИ

That's how Reason was born. Что радует отдельно - остается верным этому по сей день. Прежде чем мы перейдем к самому языку, хочется выделить то что выгодно отличает его от некоторых других compile2js языков

Несмотря на то что он general purpose, reason изначально придуман чтобы компилироваться в жс. Для F# есть Fable, есть Kotlin, есть ScalaJs, но это все second class citizens. Тут же все для веба и ради веба (точнее ради реакта, но об этом попозже)

Опять же несмотря на то что он для веба, он имеет практически полную AST совместимость с OCaml. То есть вы можете без потерь (за редкими исключениями) транслировать Reason в OCaml и обратно. Соотвественно так как компилятор один, вы вольны писать как на ризоне так и на окамле

А экосистема (в том числе пакетов) будет одна! На самом деле экосистемы две: веб и натив, но про натив будет потом Конечно не все так гладко и всякие проблемы будут, если вы будете стараться push it to the limit, но тем не менее

@jsunderhood Не уверен, что было именно так. До Reason в недрах Bloomberg был создан Bucklescript для компиляции OCaml в JS (techatbloomberg.com/blog/release-1…). И вот уже Bucklescript (сокращают до BS) использовал Facebook как фундамент для создания Reason.
Это очень важное дополнение, спасибо большое Тут стоит упомянуть то что OCaml и до bucklescript умел компилиться в js (jsoo он же js of ocaml), НО это было для ноды и для исполнения на серверах, ибо 5-мегабайтные бандлы полученные через jsoo были нормой (нативная либа) twitter.com/myshov/status/…

Bucklescript же придумал новый очень тонкий рантайм и stdlib (Belt) который позволил это все реально использовать для фронтенда Ну и еще навернул кучу штук чтобы фронтенл было легче писать и дебажить

@jsunderhood На этой неделе просто рекорд добавленных в закладки тредов. Человек аккумулирует кучу знаний в простой и быстрый overview. Респект.
Рад стараться! twitter.com/akrichevskii/s…

Язык и экосистема активно развиваются. Я помню как смотрел доклад Patrick Stapfer про reason в 2017, помню как начинал изучать и вижу какой он сейчас. Все сильно изменилось в сторону того что стало проще, понятней и логичнее

Комьюнити небольшое (по сравнению с тем же ТС), но фб действительно в это вкладывается. На дискорде сидят кор контрибютуры, прекрасно принимаются PR (испытал на своем опыта), куча дружелюбных людей которые делают крутые вещи

Это те причины почему я верю в язык и комьюнити Теперь давайте наконец-то про сам язык!

Во-первых есть полноценный вывод типов Про него я уже рассказал выше

Во-вторых есть компилятор (bucklescript.github.io) который имеет очень тонкий рантайм и стандартную библиотеку (Belt) Компилятор при этом больше похож на babel или tsc, он не делает бандлы, а просто кладет скомпилированные жс файлы рядом с оригинальными

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

@jsunderhood Ну совсем не писать нельзя. Но достаточно описать аргументы функций/классов которые экспортятся.
Спасибо, я забыл упомянуть что флоу как раз использует эту концепцию twitter.com/IAmTrySound/st…

В-третьих в нем есть функциональные компоненты, иммутабельные списки (в которые быстро писать в начало), рекорды и переменные (лол) по-умолчанию иммутабельные Для мутабельных переменных есть синтаксис и еще shadowing (когда вы переобъявлете переменную во вложенном скоупе)

В-четвертых в нем есть те структуры которые более похожи на жс, мутабельные массивы, Js.Dict для объекта как мапы и всякое другое Конечно же обо всем этом вам лучше расскажет документация, но я почему-то все еще здесь reasonml.github.io/docs/en/overvi…

@jsunderhood 🤦‍♂️Так говорят только хейтеры flow. А теперь посмотри сюда и скажи насколько flow заброшен. github.com/facebook/flow/… А разочаровались в javascript. И сделали его compilation target, а не рождественской елкой, которую обвешивают типами.
На самом деле я рад что флоу продолжает развиваться Но так или иначе фб в какой-то момент потерял доверие комьюнити из-за флоу twitter.com/IAmTrySound/st…

В-пятых в нем удобный interop с js (привет Elm) reasonml.github.io/docs/en/interop Что еще прикольней так это то что помимо того что можно писать интероп с библиотеками и js файлами еще можно использовать их типы! Есть вот такая тула которая позволит генерить ризон из flow/ts и наоборот

В-шестых в нем есть first class поддержка React reasonml.github.io/reason-react/d… Вот так выглядит функциональный реакт компонент на ризоне [@react.component] let make = (~name) => <button> {ReasonReact.string("Hello " ++ name ++ "!")} </button>;

Получается семантичненько open Path let root = create("/") let myFolder = create("saito/code") let myPath = root // myFolder

Тут я использовать еще одну фичу: синтаксис открытия модуля Вообще все модули находяться в глобальной области видимости под своим неймспейсом. Можно было бы написать так let root = Path.create("/") let myFolder = Path.create("saito/code") let myPath = root Path.// myFolder

Но Path.// выглядит особенно уродливо. К счастью есть scoped open module Path.( // мы открыли модуль пока мы в () let root = create("/") let myFolder = create("saito/code") let myPath = root // myFolder )

Но опять же, это все документация и насущное, вернемся к практике Давайте о том что же в ризоне нет/сильно недостает

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

Нет async/await. Надежды есть, в окамл 4.08 завезли сахар для монад (а промис это почти монада), похожий на computation expression в F# и с помощью него можно сделать аналог async/await. Но по срокам не подскажу

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

Но в целом только вот эти три крупные проблемы (во всяком случае которые испытал я)!

Ах, ну да, еще то что любой другой фреймворк (вуй, ангуляр) кроме реакта завести в ризоне будет крайне тяжело Либы интегрируются нормально, фреймворки (крому реакта) не особо

Тут я скину несколько ссылок Доки ризона reasonml.github.io/docs/en/what-a… Доки reason-react reasonml.github.io/reason-react/d… Доки bucklescript (компилятор в жс) bucklescript.github.io/docs/en/instal… И дискорд discordapp.com/invite/reasonml И предлагаю вам позадавать вопросы про ризон

🔥Тред (Майк Башуров)

Воскресенье


@jsunderhood насчёт типизации, смотрел Idris?
Сам руками не трогал, но coq и idris это конечно круто Кому интересно про типы youtu.be/nFtO6419A5k m.soundcloud.com/podlodka/podlo… twitter.com/iamstarkov/sta…

Did you ever implement (or try) event listener pattern by youself?
Попросили ретвитнуть, сложно отказать, ответьте на опрос если не сильно лень twitter.com/rdvornov/statu…

Сегодня мой последний день здесь, я конечно порядком подустал за неделю, но и все донести тоже не успел Поэтому последним тредом тут будет опять typescript, но на этот раз про валидацию I/O, а точнее контракта бэкенда

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

Если рассмотреть типичную ситуацию с Rest Api и Json то это будет выглядеть как-то так type User = { id: number } const getUser = (id: number) => fetch<User>(/user/${id}, 'GET') .then(r => r.json()) const user = await getUser(1)

@jsunderhood На самом деле разруливает он данный случай не так хорошо как могло быть. Есть 2 ветки систем типов: Hindley-Milner(HMTS) и implicit subtyping(ISTS). Различия стоят целого треда но благо за меня его уже написали: twitter.com/javierwchavarr… twitter.com/javierwchavarr…
Да, согласен, отличное дополнение twitter.com/_dmayorov/stat…

Но здесь на самом деле происходит неявная конвертация из any в User! Потому что на самом деле нет никаких гарантий что бэк прислал именно такой жсон. Там может быть null, там может быть опечатка в поле, да что угодно, as long as it's valid

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

Есть радикальные способы: смена способа общения на такую, которая гарантирует проверку данных: тот же protobuf скажем Есть способы борьбы через кодогенерацию, Swagger или GraphQL (так как у вас есть инфа о типах, можно сгенерить и валидатор)

Для того чтобы доказать используются type refinements (они же type guards) Выглядеть может примерно так Можете поиграться сами typescriptlang.org/play/#code/MYe… pic.twitter.com/NUvgdPLZ0A
Но что же делать если на GraphQL и даже Swagger бэкендеры не соглашаются (например это внешний сервис), а проверять хочется? Использовать голый unknown нормально не получится twitter.com/jsunderhood/st…

Наивным подходом будет просто писать валидаторы руками. Взяли какой-нибудь github.com/ianstormtaylor… и написали

import { struct } from 'superstruct' type User = { id: number } const User = struct({ id: 'number', }) const getUser = (id: number) => fetch<User>(/user/${id}, 'GET') .then(r => r.json()) .then(json => User(json)) // валидация тут

Проблема здесь я думаю налицо: как держать тип и валидатор в синхронизации? Любое изменение как валидатора, так и типа никак не проверить, все на вашей совести. Поэтому такой подход плохо скейлится на средних и больших проектах

Чтобы удобно синхронизировать их, нужно иметь single source of truth. Их потенциально тут два: либо тип, либо валидатор. Для начала рассмотрим валидатор.

Короче, можно написать валидатор и из него уже вывести тип, комбинируя валидаторы! Самый известный представитель этого подхода в ТС - github.com/gcanti/io-ts, можно еще вглянуть на github.com/pelotom/runtyp…

В целом отличный подход для старта свежего проекта, но не без недостатков Использование вывода там на полную катушку и если у вас огромные конкракты и их много, можно перегрузить ТС (я слышал о случаях когда ВебШторм помирал с io-ts, но это вполне мог быть и ТС сервер)

Развесистая фигня, требующая времени чтобы понять что тут к чему Непростая интеграция в существующий проект

Альтернативой будет взять тип и по нему сделать валидацию. Это вполне распространенный подход в ООП языках которые имеют рефлексию. Но в ТС есть проблема: рефлексия в жс в принципе есть, можно читать поля объекта, можно делать typeof. Но типов то нет!

Точнее типы конечно есть, да не те. Мы то хотим сделать валидацию по type User = { id: number }, но при компиляции он просто стирается. И тут опять же два варианта развития событий

@jsunderhood У меня всего один вопрос. Как этим пользоваться если я не упоротый функциональщик? Я честно пытался, открыл доку но так ничего и не понял(
Да, есть такое, io-ts апи очень фпшное и завязано на Either из fp-ts Я бы предложил не мучится и написать хелпер который сконвертит это в ошибку twitter.com/dskr_dev/statu…

Что-то типа import * as t from 'io-ts' import { isRight, Either } from 'fp-ts/lib/Either' const User = t.type({ userId: t.number, name: t.string }) function eitherToThrow(either: Either) { if (isRight(either)) return either.right throw new Error(either.left) }

// бросит ошибку в случае ошибки валидации const user = eitherToThrow( User.decode( JSON.parse('{"userId":1,"name":"Giulio"}') )) Это конечно не тру-ФП, но лучше уж так чем никак

Вариант номер раз: отправить информацию о типах в рантайм. Это как раз и есть reflect-metadata о которых мы говорили в среду Мощная фича, есть например вот такой github.com/typestack/clas… Проблемы следующие

Декоратор должен быть повешен на поле реального объекта или класса, то есть вы не можете написать type User = { @Guid id: string } Придется все писать на классах (а классы это ненужная в этом месте абстракция, лишний рантайм и проблемы и сериализацией/десериализацией)

Получается вот это
notion image

Я даже написал рабочую либу! github.com/ts-type-makeup… Вообще с помощью такого подхода можно делать еще много всяких штук, скажем генерить prop-types, делать фейковые бэкенды только по описанию эндпоинтов, генерить property-based tests

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

Пока это работает только через typescript transformer Я написал доки как интегрировать это с ts-loader и ts-node github.com/ts-type-makeup… Но все равно это не также легко как и babel plugin Хорошие новости в том, что принципиально это возможно github.com/milesj/babel-p…

Пока нет поддержки кастомных валидаторов А это crucial thing, чтобы можно было написать валидатор для UUID, ISO8601, целых чисел и подобной фигни

Планирую поддержать это через user type guards и Branded types import _isUuid from 'is-uuid' type Uuid = string & { readonly __brand: uniqe symbol } const isUuid = (v): v is Uuid => _isUuid.v4(v) type User = { id: Uuid } validate<User>(jsonParsed, [isUuid])

Проблема связанная конкретно с тем что мы фактически имплементируем c++ templates github.com/ts-type-makeup… Для каждого разного аргумента дженерика мы генерируем новую версию функции Но при этом при компиляции нам нужно найти эту функцию, прочитать дженерик и сгенерить нужную

Но если кто-то сделает const myValidate = <T>(jsonStr: string) => validate<T>(JSON.parse(jsonStr)); То нам придется размножать функцию myValidate, а не validate. Но я не могу знать об этом заранее.

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

Я призываю компиляторщиков которые наставят меня на путь истинный и подскажут что курить и как с этим бороться

Вот такие пироги! Ставьте звездочки, а также обратите внимание что либа имеет полный набор честных e2e (если можно так назвать) тестов github.com/ts-type-makeup…

🔥Тред (Майк Башуров)
И на этой позитивной ноте я с вами прощаюсь (в реплаях может еще поотвечаю)! С вами был Майк Башуров aka @SaitoNakamura Те темы которые пообещал не успел затронуть, попробую раскрыть в своем аккаунте, так что подписывайтесь Всем спасибо и приходите на @HolyJSconf , будет круто

🔥Тред (Майк Башуров)

Ссылки