Относительная простота и доступность Javascript, относительная легкость входа в него предопределила на многие годы вперед его популярность. Некоторые языки программирования существенно повлияли на развитие всей отрасли в целом. Так, например SmallTalk стал неким прадедушкой ООП и объектного подхода, хоть сам по себе мало где используется. А Javascript… на мой взгляд это язык, о потенциале которого создатели не имели должного представления. Но мне всегда хотелось работать с чем-то на стыке между гибкостью  Javascript и строгостью типизированных языков. Возможно ли это? В добрых традициях хабра я хочу сразу оговориться, что не претендую на применимость в 100% случаев, и не считаю мое мнение единственно верным. Приведенное ниже — мой взгляд и отношение к программированию вообще и к написанию кода на TS/JS в частности. Кроме статьи, я этот взгляд постарался донести на митапе, запись которого доступна тут. Javascript впервые увидел свет к конце 1995, он был встроен в Netscape Navigator. Сам браузер никто уже не помнит, но язык прочно прижился. Интересно то, что изначально Javascript был предназначен для относительно простых вещей, чтобы что-то интерактивное сделать на веб странице. Например анимацию, или обработку нажатий кнопок.  Дело в том, что веб в 90-х был слишком статичным, не хватало анимации и динамики, интерактивности. Основатель Netscape считал, что нужен новый скриптовый язык, способный работать с DOM. При этом — язык не должен предназначаться для разработчиков или инженеров. Для этого уже была Java. Напротив, новый язык должен был предназначаться для дизайнеров. Согласно исследованию Stackoverflow, JS используют 67% разработчиков. Чтобы мы ни говорили о языке, это что-то да значит.  Несмотря на то, что JS был рожден в спешке, в него были заложены мощные особенности, которые определили его как язык и позволили ему перерасти свои границы и роли, несмотря на его причуды. Javascript до сих пор несет с собой многие особенности, заложенные в него изначально, которые с одной стороны определяют его пресловутую гибкость, но с другой усложняют жизнь разработчикам. Все же знают шутку про две книги о Javascript по 9 долларов за 99 в сумме? Это обусловлено тем, что объекты в Javascript, ведущие себя совершенно идентично, могут быть различных типов.  В дальнейшем была долгая эволюция, появились стандарты ECMA Script, новые особенности языка. Так например появились ключевые слова let и const, промисы, классы как некая абстракция над прототипами и прочее. Если оглянуться вокруг — почти все что мы используем каждый день написано на JS, написано кровью и потом программистов и тестировщиков. Даже на бекенде — появление NodeJS во многом определило роль Javascript как full-stack языка. Я обо всем этом рассказываю, так как для меня лично это большая боль. Будучи в программировании лет 12 мне было очень трудно после многих лет типизированного C# написать хоть что-то осмысленное на JS. Более того, я могу честно сказать, что так и не смог этого сделать! Почему? Дело в том, в моей строго типизированной голове просто не укладывается как это можно без компиляции что-то сделать, да еще потом присвоить объекту какое-то новое свойство и отдать все этого в продакшен. Не было какой-то банальной точки минимального контроля. Было бы здорово соединить полезные свойства гибкого JS и строгого языка, типа C#, причем чтобы эти свойства были всегда, а не только пока пишем код. В первую очередь — JS прекрасный довольно удобный язык, порог входа низкий, а развивается он здорово в последние годы. С каждым нововведением писать становится все удобнее и удобнее. Но это интерпретируемый язык без нормальной типизации. Подробнее о видах типизации хорошо написано тут.  JS язык слабо-типизированный, в то время как TS, оставаясь динамическим, уже имеет строгую типизацию. Это помогает предотвратить большое количество ошибок, опечаток и прочего еще до первого запуска. Вы скажаете, что сейчас столько классных IDE! О каких опечатках я тут рассуждаю. И будете правы. Но IDE не дает гарантии того, что в спешке вы проверите все те подчеркивания, которые среда разработки показывает. Typescript мне помог в этом. Будучи чем-то среднем между JS и тем же C# (конечно, их же придумал один и тот же человек — Андерс Хейлсберг). Интересно еще то, что он же придумал Turbo Pascal и Deplhi — которые в свое время были буквально промышленными стандартами. TS дает нам компиляцию, дает типы! Аллилуйя! Если вы теперь накосячите с именем переменной то узнаете об этом сразу, а не через неделю от разгневанного заказчика. Это же великое благо! Именно с TS началась моя реальная работа как веб программиста. Но Typescript не решает фундаментальных проблем, присущих JS и проявляющихся во всей красе во время исполнения кода. Язык не был бы языком программирования, если бы на нем нельзя было ничего сломать. У TS есть свои проторенные дорожки в старую добрую гибкость JS. я уже неоднократно порываюсь подобный слайд убрать из каждой своей презентации, но вижу как часто везде используют ANY. Поэтому поговорим о нем минутку. TS позволяет писать код с типами, функции с типами входных и выходных данных, и даже делать обобщенные типы. При этом все еще сохраняется лазейка обратно. Любой явно неуказанный тип — это ANY тип. Я часто думаю, что про ANY (еще про unknown) уже достаточно сказано, но нет. Во многих проектах пруд пруди всяческих ANY там, где их использование не обусловлено необходимостью. Поэтому приведу простой пример. Тут все в порядке, все работает. Но вот мы делаем рефакторинг и решаем, что слово name плохо передает смысл названия фильма, а вот слово title — гораздо лучше. После изменения все продолжит работать и компилироваться на TS, ведь наша функция ожидает ANY на вход. И только где-то на UI мы увидим слово undefined. В поиске этого бага можно потратить много бесценных часов. Если же отказаться от ANY и написать функцию с нормальным типом на входе: То в случае частичного рефакторинга мы сразу увидим ошибку компиляции. TS2339: Property 'name' does not exist on type 'Movie'. Кстати, если вы себе вдруг не доверяете, можно жестко включить no-any правило в линтере (больше информации тут). Все бы хорошо, и мы уже почти подошли к сути, вот только после компилирования TS получается что? Чистейший JS, который и выполняется в браузере или другом движке. А это значит, что все наши типы и проверки было лишь локально в процессе разработки Вообще, статический анализ кода позволяет избежать громадного количества ошибок, даже в сильно типизированных языках. Я как-то смотрел видео с конференции dotNEXT, в котором выступающий утверждал, что статический анализатор кода их компании помогает избежать 40% ошибок в C# коде еще до запуска программы! Вдумайтесь только, они же статистику собирали по проектам. Давайте посмотрим, как TS выглядит в деле. Для простоты примера возьмем простую функцию: В TS у нас есть класс, публичная функция в этом классе, везде проставлены типы. если мы что-то сделаем не так пока пишем код, то компилятор TS выдаст ошибку. Например такую:  Давайте скомпилируем код функции в Javascript командой tsc в директории проекта. Полученный результат выглядит следующим образом: Легко заметить, что никаких типов у нас уже нет, в рантайме будет сразу попытка выполнить операцию. В зависимости от реальных аргументов, события могут развиваться по разному: Результат NaN будет для всех случаев, когда JS не знает что делать в математической операции. Если же мы имеем дело с различного рода объектами, и функция получит объект, все равно будет попытка работы с полями объекта. Соответственно мы рано или поздно получим плохо отлавливаемый undefined. Ну хорошо, в принципе ничего неожиданного. JS гибок как никто другой. Все приведенное выше логично и ожидаемо от JS. Работая с TS мы все равно делаем шаг вперед и имеем по крайней мере статический анализ на такого рода ошибки. То есть в идеальном случае, когда мы контролируем 100% кода и тестируем 100% сценариев, проблем быть не должно. Где же вообще мы можем натолкнуться на ситуацию, когда передаваемый тип отличается от ожидаемого, если TS компилятор все за нас проверил? Слегка надуманные кейсы: Более реальные случаи: Я задумался над этим по нескольким причинам. Во-первых, я пришел из C++ и .NET, и мне чрезвычайно близка идея строгой типизации. Что впрочем не мешает мне работать с TS уже несколько лет. Его (и JS конечно же) бенефиты в виде бесшовной работы с JSON покрывают многие проблемы, а скорость работы NodeJS сейчас позволяет делать на нем многое. Во-вторых, у меня на проекте есть необходимость проверки такого рода, мы делаем постепенную миграцию старого не очень хорошо написанного кода из смеси JS / TS на более структурированный чистый TS. И какое-то время классы могут использоваться как старым кодом, так и новым. Нужна удобная проверка на ошибки, но так, чтобы не писать instance of каждый раз.
Эти проблемы вполне успешно решаются валидацией различного рода. Например, в случае сервера, мы можем добавлять middleware или что-то подобное и выполнять код, валидирующий все нужные запросы. Чуть дальше я приведу примеры. Сложные случаи с бизнес логикой как правило так и решаются. Но мне не хватет чего-то простого и очевидного. Например: Если вызвать некорректно будет следующее: Естественно писать так на каждую фукнцию, да еще и поддерживать в дальнейшем, весьма сомнительная простота. Поэтому давайте немного оптимизируем этот код с точки зрения масштабируемости. А именно напишем декоратор. А функция проверки может выглядит таким образом: Вызывать его можно довольно удобно, просто вешать декоратор на нужную функцию: В случае некорректных аргументов мы наблюдаем ошибку. Ее следует либо ожидать с помощью привычного try / catch, либо логировать как-то еще. В любом случае теперь непрошенный аргумент не пройдет и будет замечен, при этом для использования достаточно просто повесить декоратор на функцию без какой-либо настройки. Это конечно базовая версия, написанная для демонстрации идеи, она не будет работать в 100% случаев, но передает суть идеи. Рассмотрим чуть детальнее код декоратора. Как вы знаете — декоратор, это всего лишь определенного вида функция. На вход она получает: По сути план действий тут прост: Сами проверки становятся возможны за счет Typescript и метадаты. Вызов Reflect.getMetadata('design:paramtypes', target, propertyName) возвращает нам список параметров исходной функции, а текущие аргументы мы можем получить классическим для JS способом — через переменную arguments. Стоит только отметить один момент, каждый из параметров в возвращаемом значении Reflect.getMetadata будет по сути функцией, поэтому чтобы узнать сам ожидаемый тип, можно либо проверить поле name, либол эту функцию вызвать, и только после этого использовать typeof. Разумеется, это решение можно и нужно развивать до того состояния, когда ваша задача будет выполнена. Но уже в таком простом виде можно смело использовать. Этакая симуляция строгой типизации в JS / TS. Возникает закономерный вопрос о производительности. Давайте сравним чистый вызов с умножением пары чисел, и такой же, но обернутый декоратором. Замеры производились на коленке с помощью console.time и console.timeEnd Чистый вызов функции в среднем: 0.08ms Вызов функции с декоратором в среднем: 0.3ms.  Это замеры после прогрева, то есть второй и последующие вызовы. Хотя это довольго субьективно, цифры скачут очень сильно. Само по себе не критично, хотя и достаточно много. По сути условные 0.22 ms отнимается проверкой на каждый вызов. Часть из этого отнимается на получение метадаты, но если сравнить — то это время даже меньше чем время выполнения умножения (если брать цифры второго и последующих запусков). Довольно много занимает forEach, если переписать на обычный for, картина станет приятнее. Сами по себе цифры не показательны, но некий тренд увидеть можно — минимальное влияние метадаты и то, что лучше использовать простые конструкции типа for там, где критична скорость.
. First Run Second Run Я подготовил небольшой npm пакет, который содержит первую версию декоратора @Typed, его можно скачать и попробовать в деле — https://www.npmjs.com/package/ts-stronger-types. Я буду признателен, за реальную обратную связь в виде issue на github. Для полноты картины стоит затронуть существующие решения, позволяющие делать простую или продвинутую валидацию. Данная библиотека построена на принципе преобразования объектов, чем-то больше напоминает Модели Данные в некой ORM, нежели обычную валидацию. Предполагается, что классы вы описываете в следующем виде: А далее при работе с обьектом делаются все необходимые проверки. В репозитории по ссылке есть вся подробная инфа. Мне симпатичен данный подход, но я подозреваю что его использование означает перевод всего приложения на данные типы. Эта интересная библиотека взяла за основу другой подход. Типы остаются обычными TS типами, но у нас есть возможность вызова функции проверки: Возможно это один из наиболее дешевых способов проверить ответ с сервера (или запрос клиента) на типовую валидность. Но есть и ограничения: Эта библиотека ближе к изначальной идее, которую я описал в статье. По сути тут вы можете декораторами навешивать нужню валидационную логику: А далее в любой нужный момент вы можете вызвать функцию validate из этой библиотеки для проверки, функция вернет Promise. Соответственно на catch можно увидеть валидационную ошибку. В репозитории есть подробные примеры. Эта библиотека интересна еще и тем, что она используется в качестве основной для проверки в известном фреймворке NestJS. Не углубляясь в подробности фреймворка (этому можно уделить отдельную статью), у вас из коробки есть возможность включить валидацию запросов на уровне всего API, либо на уровне конкретного контроллера или обрабортчика. Оба примера включают проверку запросов перед вызовом конкретного обработчика. Проверка осуществляется с помощью class-validator. Пожалуй, одно из основных качеств хорошего кода — это его предсказуемость. Когда программа работает так, как вы ожидаете в любой ситуации. В данной статье я постарался изложить некоторые мысли и подходы о том, как снизить количество нештатных ситуаций при работе с Typescript и тем самым увеличить предсказуемость программы. Лично для меня использование валидации в рантайме немного приближает JS / TS к привычным для меня языкам с более строгой типизацией, уменьшая тем самым потенциальное количество ошибок. Таким образом получается совместить положительные качества гибкого интерпретируемого Javascript и строгого языка типа C#. Тем самым инвестируя свое время в проектирование и написание кода, а не в бесконечное отлавливание ошибок. Пишите качественный предсказуемый код с удовольствием! Full Stack Software Developer