JavaScript to 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 все же удобнее. Придерживайтесь стиля «один файл — один класс».

upd. Если использовать проект студии HTML Application with TS — то это опять же не нужно, но я, например, использую проект ASP.NET — он ничего не знает о TS, кроме того, что я ему указываю как генерировать в post build. Поэтому думаю если у вас хоть более менее сложный проект (а зачем иначе вам переходить на TS?) или вообще вы не используете проект вы не будите использовать проект вида HTML Application with TS.

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


С присвоением ссылки на функцию, т.е. с созданием калбэка тоже есть непонятки. Но они скорее дисциплириуют, но могут адептам 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 часть всего класса, поделить на ряд сваязанных интерфейсов, после чего передавать ссылки не на весь класс, а ссылки на выделенный интерфейс.
6. еще мелочь — есть enum`ы
7. а еще приятно, что есть т.н. необязательные параметры, т.е. те которые функции передавать не нужно, и что важнее те которые в интерфейсе можно не реализовывать. Это порой наверно даже лучше, чем в C# — где нужно отделаваться реализациями вида return null (на что мой шеф однажды пошутил, что если бы он знал как такие реализации делать в коммерческом смысле возвращая в качестве реализации [тут нечто не читаемое], то его клиенты были бы особенно рады :) ).

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

upd.

Люди, объясните некоторым ниже кто выступает с большим апломбом (тем более при этом выражая свои заблуждения), чем интерфейс отличается от этого
Вы тоже самое получите просто помечая члены класса как public \ private


и аналогичный холивар/троллинг разводить мне не интересно, есть вопросы — напишите в личку отвечу.

В данном случае, declare ничего не объявляет, а указывает компилятору, что во время выполнения будет такая переменная с таким типом, чтоб он не ругался на её использование.


И что? В статье явно написано, что это может привести к использованию глобальной переменной. Непонятно как — перечитайте статью.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 40

    +1
    Мы перевели сервер на NodeJS на тайпскрипт + файберы и невероятно счастливы. Тесты стали писаться проще, кода работает стабильнее, качество кода повысилось. Что бы выкатить какую-нибудь лажу типа there_is_typo_in_this_fction() уже и забыли. Немного разражает отсуствие байндингов для свежих версий библиотек, и время компиляции.
      0
      Я тоже как то попробовал небольшой проект node начать на TS, меня глубоко возмутило время компиляции, тем более при использовании плагина watch с gulp.js. В виду невозможности терпеть подобное проект пришлось откатить на js
      –3
      тень необъектного JS

      Перестал читать на этой строчке.
        +10
        Строгая / статическая типизация основная фича, остальное и в JS нормально реализуется.
          +1
          Так вот кто знаком с Cи конструкция reference хоть и находится формально под комментарием является аналогом include. И самое главное, код перестанет работать если вы не в том порядке подключите скрипты.

          Начиная с 0.9.1 (на дворе уже давно 1+) конструкция reference нужна только для ссылок на файлы вне проекта.
            0
            О каком проекте речь? проект VS?
              +1
              Да.

            +1
            пофайлово видеть код на JS все же удобнее

            Так генерируем code map и браузер в отладчике автоматом подтянет все исходники на typescript.
              0
              А под node.js? Там всё уже не так тривиально.
              0
              использую проект ASP.NET — он ничего не знает о TS, кроме того, что я ему указываю как генерировать в post build

              У вас какая студия? У меня в 2013 такой же проект (мигрированный с 2012), и там прекрасно все работает без референсов.

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

                  What?

                  (1) В TS нет никаких «непоняток» с передачей делегатов, эта передача работает точно так же, как в JS.
                  (2) В .net вообще и в C# в частности делегаты передаются так же (с точки зрения программиста), как и в JS, разница в поведении this.
                  (3) Что небезопасного в том, как передаются делегаты в JS?
                  (4) Передача ссылок (точнее, указателей) и передача делегатов — две совершенно разные вещи, непонятно, зачем вы их впихнули в один абзац.
                    +16
                    Как человек, написавший на TS несколько проектов «клиент на Angular + сервер на Node» и в последствии перевевший на него всю свою команду, я попросил бы вас статью убрать в черновики (слишком много фактических ошибок) и прежде чем писать на TS и о TS прочесть JavaScript: The Definitive Guide, 6th Edition by Flanagan (JavaScript. Подробное руководство, 6-е издание).

                    «В TypeScrip просто не объявляйте внешних переменных с помощью declare var.» В данном случае, declare ничего не объявляет, а указывает компилятору, что во время выполнения будет такая переменная с таким типом, чтоб он не ругался на её использование. За счет этого работают файлы деклараций для сторонних библиотек.

                    «TypeScript поддерживает настоящие наследование, чем оно отличается от JS наследования поговорим позже.» Ничем. Вы вообще смотрели внутрь файлов, которые вам сгенерил TS? Посмотрите вот тут хотя-бы дефолтный пример www.typescriptlang.org/Playground. Классы TS это удобный сахар над классами JS с их прототипным наследованием. Если быть более точным, это даже не сахар, а синтаксис ECMAScript 6, который сейчас потихоньку внедряется в браузеры, но в TS просто обратно компилится в ECMAScript 3/5.

                    «Получаем такой бонус как полноценную поддержку конструкторов в классах. Без них надо изобретать т.н. фабрики с методами init()» — см. пункт выше про классы.

                    «Мы получаем строгую типизацию» нет, мы получаем только её проверку на этапе компиляции.

                    «Скажу лишь одно, с помощью интерфейсов реализуется множественное наследование, и легко можно сложный интерфейс public часть всего класса, поделить на ряд сваязанных интерфейсов, после чего передавать ссылки не на весь класс, а ссылки на выделенный интерфейс.» Вы тоже самое получите просто помечая члены класса как public \ private. Обратите, кстати, внимание, что в скомпиленном JS они всегда public. То-есть, имея private свойство вы все равно можете сделать myVar['privateVarName'] в TS, а в JS и этого не надо.

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

                      –21
                      А вы не пишете свои глупости и на этом разойдемся.
                        +8
                        Я так понимаю, что по существу ответить нечего?
                          –16
                          Ну, я рад, похоже уже и без меня разобрались… надо еще что-то ответить или разберетесь со школьником сами?
                            +7
                            Ну вообще-то в комментарии KIVan было намного больше одного пункта, а вы не смогли аргументированно ответить ни на один. Так что пока что выходит так, что вам по существу сказать как не было ничего, так и нет.

                            Ну и не стоит называть школьником человека, который сказал что-то, с чем вы не согласны.
                          +6
                          Скромнее надо быть. Он пишет всё по делу, а вы только только ознакомились с TS и едва знакомы с JS и уже пытаетесь тыкать кого то носом.
                            –13
                            Да, нет там ничего по делу :) Объяснять очевидности мне тут не интересно. Но если вам реально интересно, пишите в личку — попробуем разобраться. А так это очередной холивар, мне он не интересен.
                              +1
                              Опыт общения с вами «в личке» показывает, что вы там ничем не отличаетесь от здесь. Так что особого смысла нет.
                                +2
                                Мы уже подробно разбирались с вами по поводу того самого кода с глобальным xmlhttp. На должность стажёра я бы вас взял — потенциал у вас явно есть, а опыта почти нет (зато вместо опыта есть много заносчивости). То, что вы решили разобраться с TS это похвально, а вот то, что вы пытаетесь преподнести своё трёхдневное знакомство с TS как глубокое понимание предмета — это не похвально. Кроме того, судя по ошибкам в статье, вы также мало знакомы с C# (с JS вы точно мало знакомы — мы уже разбирали ваш код в личке).
                                  –9
                                  Ну, и кто это здесь заносчив :) заканчивайте говорить в стиле Ad hominem — это Вас не красит.

                                  " трёхдневное знакомство с TS как глубокое понимание предмета"

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

                                  «судя по ошибкам в статье, вы также мало знакомы с C#»

                                  смешно :) Может стоит вначале назвать в чем ошибка, а потом уже что-то утверждать?

                                  P.S.
                                  Приезжайте к нам, может и я устрою вас на работу :)
                                    0
                                    Однако, мне этого было достаточно, чтобы перевести мое проектик.

                                    Чтобы «перевести проектик» на TS достаточно пары часов — потому что TS работает с JS side-by-side (а интеграцию вы до сих пор не доделали, да и на вопрос с VCS не ответили, что тоже показательно). А если говорить о том, что вы весь JS-код переписали на TS — то тоже особо хвастаться нечем, потому что с одной стороны непонятен объем, а с другой — насколько реально вы пользуетесь возможностями TS. any => any писать просто…

                                    Например, явно видно, что вы еще плаваете в том, как именно TS захватывает и трактует контекст (иначе бы вы не писали, что «дело в том, что теперь на уровне класса вы больше не можете объявлять локальные переменные с помощью var.»).

                                      0
                                      На ошибку указал lair:

                                      (2) В .net вообще и в C# в частности делегаты передаются так же (с точки зрения программиста), как и в JS, разница в поведении this.
                                        –1
                                        И где же тут ошибка? Я разве где-то написал другое? Я именно про разное поведение this и написал, а lair переписал это только своими словами и ничего более :)
                                          0
                                          Вы написали другое, детали есть в моем комментарии выше, ответить на который вы тоже не удосужились.
                                      +3
                                      На должность стажёра я бы вас взял


                                      Не стоит. С таким уровнем можно брать 20-ти летних. А тут случай тяжелый, мужику за 30 уже, в голове каша, фундаментального понимания программирования нет, желания учиться, судя по всему, тоже.
                                        –3
                                        Написал 20-летний стажер :)
                                          +1
                                          Понятно, даже посмотреть в юзеринфо теперь лень (ну или считать не умеете). Мелко.
                                            +1
                                            Не стоит заострять внимание на возрасте. 20-ти летний стажёр который кодит лет пять (а есть такие, которые начинают этак лет в 7) будет во всех отношениях лучше большого и взрослого дядьки который только только начал что то там кодить. Вон, например, Магнус Карлсен обставлял всех в шахматы уже в 17 лет.
                                              0
                                              Хвастаться надо не возрастом, а рейтингом AGA или ФИДЕ.
                                                0
                                                Хвастаться виртуальными бонусами еще смешнее :) Может лучше все же годами опыта?
                                                  0
                                                  Годы опыта сами по себе не значат ни-че-го.

                                                  (удивительно, что вы свой единственный, если не ошибаюсь, разрешенный комментарий в день тратите на petty bickering, а не на ответ по существу. Ну или не очень удивительно.)
                                                    0
                                                    Уж лучше тогда зарплатой — она всё таки более материальна чем годы эфемерного опыта.
                                    +3
                                    «легко можно сложный интерфейс public часть всего класса, поделить на ряд сваязанных интерфейсов, после чего передавать ссылки не на весь класс, а ссылки на выделенный интерфейс.» Вы тоже самое получите просто помечая члены класса как public \ private.

                                    Справедливости ради — не то же самое. Интерфейсы позволяют формировать и навязывать необходимый потребителю контракт объекта, при этом потребителю не важно, какого именно типа в реальности объект — достаточно знать, что он реализует конкретный интерфейс. В JS это исторически делалось с помощью duck typing, интерфейсы же делают это более формальным образом.
                                    +1
                                    «Мы получаем строгую типизацию» нет, мы получаем только её проверку на этапе компиляции.

                                    В общем-то очевидно что речь идет о type safe и строгой типизации на этапе разработки (что в принципе очень большое подспорье если кода много и пишут его разные люди), TP ведь не встраивается в браузер как интерпретатор / VM, а просто транслирует потом код в JS.
                                      +2
                                      class Menu extends RequestData


                                      Простите, это у вас действительно такая, кхм, архитектура? Или просто неудачный пример?
                                        0
                                        А что такого? У нас есть модель (RequestData) и есть для него кхм… вид (view) — т.е. Menu. А слово extends выражает некую связь этих двух сущностей. Получился, таким образом, MVVM (view model там где то тоже наверняка есть). Вот, собственно, я и раскрыл замысел автора :)
                                        +2
                                        Если бы вы поковыряли TS и JS хотя бы пару лет, то написали бы статью совсем по другому. Ну и немного скромности определённо улучшит стиль изложения и избавит от некоторых ошибок.
                                          0
                                          declare var Window: new (a: any) => any; — как-то не очень понравилось такое написание, имхо весь смысл TypeScripta теряется…

                                          Only users with full accounts can post comments. Log in, please.