🔥

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


Начнем с начала тайпскрипта, то бишь с его прошлого Появился он еще в 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)

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

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

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

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

В 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 у вас?

Дело в чем. Раньше когда вы не знали что будет за тип, был один выход: 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 и ТС усом не поведет

Тем не менее само наличие 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 }) и эффект будет похожим за тем исключением что остальные поля объекта останутся мутабельными

В 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…

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