JavaScript to TypeScript — трудности перевода

в 11:13, , рубрики: javascript, TypeScript, Веб-разработка, Программирование, Проектирование и рефакторинг

Наверно многие в курсе, что у JS достаточно ограниченно реализовано ООП. Одних уровень ООП в JS устраивает, другие не видят необходимости придерживаться правил ООП, другие без ООП не могут писать код. Тут мы попробуем без холивара разобраться в некоторых ньансах перехода с JS на TS.

О мотивации перехода мы поговорим в заключении статьи и скорее для тех, кто понимает важность качества кода. Но пару слов все же скажем вначале. Когда Вы делаете небольшой тестовый код, с неясным коммерческим статусом — то вряд ли вы будите этот код прилизывать. А ООП это хороший способ прилизать код, это не сколько не влияет на функциональность вашего кода, даже наоборот, часто задерживает быстрое написание тех фич, которые вы решили сделать. Иногда даже страдает производительность. Но наверное каждый знает тот уровень, когда ему самому уже сложно разобраться в своем коде, тогда вы начинаете его просматривать и время от времени подумывать о рефакторинге. Если ваш язык интерпретируемый, без строгой типизации и не достаточно хорошо поддерживает ООП, то вы этот момент будет оттягивать долго — но я рекоммендую все же об этом задуматься. Если ваш язык JS — хорошим вариантом будет его перевести на TS, вы ничего не потяряете это уж точно. Но есть некоторые сложности, из-за которых в процессе перевода вы можете засомневаться в правильности такого решения.

Глобальные переменные — зло

Если вы уж решили придерживаться ООП — откажитесь от глобальных переменных ВООБЩЕ. В JavaScript порой используют для этого директиву «use strict»;

В TypeScrip просто не объявляйте внешних переменных с помощью declare var. Делайте объявления только внутри классов, хотя бы так MyVar: any.

Наверное каждый может рассказать историю, почему глобальные переменные зло. Я расскажу свою. Можно сказать по ошибке я объявил одну переменную через declare var xmlhttp. Ну тогда мне показалось, что о ней как о классе TypeScript ничего не знает, а значит это кандидат на внешнию переменную. Генератор TypeScript это объявление при переводе на JavaScript проигнорировал и так она стала глобальной переменной. Ну, а так как в этой переменной содержалась ссылка на XMLHttpRequest, который обеспечивает асинхронное получение данных с сервера, то впоследствии, конечно, это приводит к багу как только вы одновременно будите получать через эту переменную разные типы данных. Причем некоторые браузеры будут это нивелировать, код даже сможет работать, но существенно замедлится и будут происходить отказы по ошибке синтаксиса JS. Вы ведь не всегда проверяете в коде перед использованием переменной, а задана ли она?

Порядок скриптов при наследовании

TypeScript поддерживает настоящие наследование, чем оно отличается от JS наследования поговорим позже. Объявляется оно так:

/// <reference path="RequestData.ts" />

module CyberRise
{
    export class Menu extends RequestData
    {
    }
}

Объявлен класс Menu наследуемый от RequestData в модуле CyberRise. В JS это генерируется зубодробительной конструкцией, которую я тут описывать не буду. По сути там трехуровневое вложение функций (и даже больше), целью чего является разграничение областей видимости. Используя модули мы можем не беспокоится о одинаковых названиях классов в разных библиотеках. А ведь действительно каждый разработчик любит использовать общеупотребительные название, даже такие как Window.

Еще Вы можете заметить конструкцию reference в начале файла. Дело в том, что TypeScript проверяет соответствие типов и ему еще на этапе компиляции (генерации JS) надо знать все о типах. Опять же преимущества строго типизированных языков я думаю все знают, отмечу самое прямое — проверка ошибок связанных с неправильным использованием объектов на этапе компиляции. Это сильно экономит время отладки в достаточно больших проектах.

Так вот кто знаком с Cи конструкция reference хоть и находится формально под комментарием является аналогом include. И самое главное, код перестанет работать если вы не в том порядке подключите скрипты. В таком варианте не работает:

script type=«text/javascript» src=«js/Menu.js»>
script type=«text/javascript» src=«js/RequestData.js»>

а так работает:

script type=«text/javascript» src=«js/RequestData.js»>
script type=«text/javascript» src=«js/Menu.js»>

Ну, собственно, это аналогично С++, там тоже include должны быть в определенном порядке. Только так как объявление скриптов делается не в TypeScript он не может это проверить на этапе компиляции и вы можете долго находится в прострации не понимая почему не выполняется ваш код. Хотя есть вариант когда весь код компилируется в один js файл, тогда компилятор TS гарантирует сам правильное расположение порядка кода. Но для серьезного проекта это может создать не удобства, т.к. пофайлово видеть код на JS все же удобнее.

Калбэки, делегаты и прочие синонимы

С присвоением ссылки на функцию, т.е. с созданием калбэка тоже есть непонятки. Но они скорее дисциплириуют, но могут адептам JS показаться не естетвенными. Те кто привык к JS частенько, думаю, занимаются передачей ссылок на функцию, даже не думая о безопасности этого. К примеру, в .NET намеренно запритили прямую работу с сылками, т.к. это приводит часто к багам. Но при ООП это часто не нужно, вместо этого передаются ссылки на объекты, и затем получающий объект использует public часть класса, используя нужные ему члены класса. Еще лучше если класс реализует интерфейс, и передается ссылка на интерфейсы. Увы, этого в JS нет, и поэтому часто пользуются не безопасной передачей ссылок на функцию.

Итак, код вида:

        xmlhttp.onreadystatechange = this.OnDataProcessing;

Работать у вас больше не будет. Точнее будет, но не в том контексте, т.е. надо использовать костыль JS

        xmlhttp.onreadystatechange = this.OnDataProcessing.bind(this);

Дело в том, что теперь на уровне класса вы больше не можете объявлять локальные переменные с помощью var. Теперь все, что вы объявляете на уровне класса = это свойства, используемые через this. Поэтому если раньше на такой костыль закрывали глаза, то теперь он выглядит еще более не естественно.

Поэтому лучше уж будет использовать аннонимные методы, чтобы устранить преследующую тень необъектного JS

            this.xmlhttp.onreadystatechange = () =>
            {
                        alert(this.xmlhttp.responseText);
            }

Внешние вызовы

Частенько мы уже пользуемся теми бибилиотеками, которые разработаны в JS. И там часто предлагают использовать уже разработанные объекты-функции. Если они вызываются через new, как то

                win = new Window({
                    className: "mac_os_x", title: locTitle, width: 1000, height: 600,
                    destroyOnClose: true, recenterAuto: false
                });

компилятор TypeScript сообщит вам, что ничего не знает о таком классе Window (он то думает, что это класс :) ). Действительно мы наверняка еще словим, что класс Window уже объявлен. Но это будет не тот класс и не стой бибилиотеки, которую мы подражумеваем. Но если мы использовали модули, то все будет нормально. А чтобы компилятор понял, что это за класс нам надо описать сигнатуру, аналогично тому как мы это делаем используя методы из dll.

Написав

   declare var Window: new (a: any) => any;

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

Мотивация

Мотивация такого перехода с JS на TS у каждого может быть своя. И говоря о ней мы рискуем халиварить. Поэтому я ограничусь тем списком, который важен для меня, когда я начинаю структурировать код и мне нужны для этого инструменты (пусть многие это сочтут за синтаксический сахар, но за ним стоит не только синтаксис, но и глубинная реализация (или не до реализация) концепций в языке):

1. За всеми попытками разделить данные на модули, классы, объекты, наличичие наследования — стоит простое желание разграничить области ответственности свойств и методов, а на уровень глубже разграничить области видимости переменных. Для этого TS выворачивается как только может, используя единственную для этого возможность в JS используя замыкания для инкапсуляции данных. Так он вводит понятие класса — как функции в функции, где одна функция это статические данные/методы/конструкторы на уровне класса, а во вложенной функции данные объекта.
2. Мы получаем строгую типизацию
3. Получаем такой бонус как полноценную поддержку конструкторов в классах. Без них надо изобретать т.н. фабрики с методами init()
4. Наследование становится не реализацией агрегации, как это по умолчанию в JS — а реальным наследованием, с контролем задания нужных конструкторов в наследниках. Чем наследование лучше агрегации? Да, не лучше, но часто нужно, когда ты разрабатываешь и перекрываешь поведение ряда базовых классов. Да и потом семантически это одношение вид чего-то (is a), а не часть чего-то (part of) — без разделения этой семантики все превращается в одну кашу, где отношения использования, агрегации, наследования становится одним и тем же.
5. Получаем еще один бонус — интерфейсы, как описательная часть классов. Объяснить их важность порой сложно, я и не буду пытаться. Те кто знает как их использовать оценят это, иные нет. Скажу лишь одно, с помощью интерфейсов реализуется множественного наследование, и легко можно сложный интерфейс public часть всего класса, поделить на ряд сваязанных интерфейсов, после чего передавать ссылки не на весь класс, а ссылки на выделенный интерфейс.

Пожалуй и все, если оцените через времечко напишу про трудности перевода баз данных — миграции с MySQL на MS SQL Server.

Автор: tac

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js