Статьи

8 шагов к переходу с JavaScript на TypeScript

Недавно мы перевели наш браузерный агент RUM из JavaScript в TypeScript. Хотя это было непросто, нам нравилось видеть, как изменения принесут нам пользу, и было интересно изучать новый язык в процессе. Позвольте мне рассказать немного о том, как мы перешли на TypeScript, о некоторых возникших трудностях и о том, как мы их решили.

Почему TypeScript

До перехода на TypeScript наш агент браузера RUM имел тысячи строк кода, но был подавлен только в два файла JavaScript.

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

Изучив такие языки, как TypeScript, CoffeeScript и PureScript, мы решили использовать TypeScript по нескольким причинам:

  1. Статическая печать
  2. Модуль и Классы
  3. Расширенный набор JavaScript, проще для изучения для разработчиков JavaScript
  4. История успеха от нашей передовой команды

8 шагов к переходу на TypeScript

  1. Приготовься

  1. Переименовать файлы

Мы переименовали все js-файлы в ts-файлы, и так как TypeScript — просто расширенный набор JavaScript, вы можете просто начать компиляцию ваших новых ts-файлов с помощью компилятора TypeScript.

  1. Исправить ошибки компиляции

Было довольно много ошибок компиляции из-за статической проверки типов компилятором. Например, компилятор будет жаловаться на код js ниже:
Пример первый

var xdr >= >window.XDomainRequest;

Решение

// declare the specific property on our own
interface Window {
    XDomainRequest?: any;
}

Поскольку «XDomainRequest» — это свойство, предназначенное только для IE, для отправки междоменного запроса, оно не объявляется в файле «lib.d.ts» (это файл, объявляющий типы всех общих объектов JavaScript и API в браузере, и на него ссылаются компилятор машинописного текста по умолчанию).
Вы получите сообщение об ошибке «TS2339: свойство« XDomainRequest »не существует для типа« Окно ».».
Решение состоит в том, чтобы расширить интерфейс Window в «lib.d.ts» с помощью необязательного свойства «XDomainRequest».

Пример второй

function foo(a: number, b: number) {
    return;
}

foo(>1);

Решение

// question mark the optional arg explicitly
function foo(a: number, b?: number) {
    return;
}

Необязательные аргументы функций должны быть помечены явно в машинописном тексте, или это дает «ошибку TS2346: предоставленные параметры не соответствуют какой-либо сигнатуре цели вызова».
Решение состоит в том, чтобы явно использовать «?» пометить параметр как необязательный.

Пример третий

var myObj >= {};
myObj.name >= >"myObj";

Решение

// use bracket to creat the new property
myObj[>'name'] >= >'myObj';
// or define an interface for the myObj
interface MyObj {
    name?: string
}

var myObj: MyObj >= {};
myObj.name >= >'myObj';

При назначении пустого объекта «{}» переменной компилятор машинописного текста выводит тип переменной как пустой объект без какого-либо свойства.
Таким образом, доступ к свойству «name» приводит к «ошибке TS2339: свойство« name »не существует для типа« {} »».
Решение состоит в том, чтобы объявить интерфейс с необязательным свойством name.

Забавно исправлять эти ошибки, и вы узнаете о языке и о том, как может помочь компилятор.

  1. Исправить тестовые случаи

После успешного получения файла JavaScript из этих файлов ts мы запустили тесты для новых файлов JavaScript и исправили все ошибки.

Одним из примеров неудачных тестов, вызванных переходом на TypeScript, является разница между этими двумя способами экспорта функции:

export function foo() {}
export var foo >= function() {}

Предполагая, что ваш оригинальный код JavaScript:

var A >= {
    foo: function() {},
    bar: function() {foo();}
}

Тестовый пример показывает:

var origFoo >= A.foo;
var fooCalled >= false;
A.foo >= function(){fooCalled >= true;};
A.bar();
assertTrue(fooCalled);
A.foo >= origFoo;

Если переписать TypeScript для JavaScript это:

module A {
    export function foo() {}
    export function bar() {foo();}
}

Тестовый случай провалится. Можешь сказать почему?

Если вы посмотрите на сгенерированный код JavaScript, вы сможете понять, почему.

// generated from export function foo() {}
var A;
(function (A) {
    function foo() { }
    A.foo >= foo;
    function bar() { foo(); }
    A.bar >= bar;
})(A >|| (A >= {}));

В тестовом случае, когда заменяется A.foo, вы просто заменяете свойство «foo» A, но не функцию foo, функция bar по-прежнему вызывает ту же функцию foo.

export var foo >= function(){}

может помочь здесь

Машинопись

module A {
    export var foo >= function () { };
    export var bar >= function () { foo(); };
}

генерирует

// generated from expot var foo = function() {}
var A;
(function (A) {
    A.foo >= function () { };
    A.bar >= function () { A.foo(); };
})(A >|| (A >= {}));

Теперь мы можем заменить функцию foo, вызываемую A.bar.

  1. Код рефакторинга

Модули и классы TypeScript помогают организовать код модульным и объектно-ориентированным способом. Зависимости указаны в заголовке файла.

///<reference path=“moduleA.ts” />
///<reference path=“moduleB.ts” />
module ADRUM.moduleC.moduleD {
    ...
}

Одна вещь, которая мне нравится при компиляции ts-файла, это использование опции «–out» для объединения всех прямо или косвенно связанных ts-файлов, поэтому мне не нужно использовать requirejs или browserify для той же цели.

С TypeScript мы можем определять классы классическим способом наследования, а не прототипным способом наследования, который более знаком программистам на Java и C ++. Однако вы теряете гибкость, которую обеспечивает и JavaScript.

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

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

Но так же, как переход от сборки к C / C ++, по большому счету, это все еще хорошая вещь.

Мы не удосужились добавить всю информацию о типе в существующий код, но нам нужно будет это сделать при изменении или добавлении кода.

Также стоит переместить тестовые наборы в TypeScript, поскольку тестовые примеры могут автоматически обновляться при рефакторинге кода в IDE.

  1. Исправить минификацию

Не удивляйтесь, если минификация нарушится, особенно если вы используете Google Closure Compiler с расширенной оптимизацией.

Проблема 1: мертвый код был ошибочно удален

Усовершенствованная оптимизация имеет функцию «удаления мертвого кода», которая удаляет код, который распознается компилятором как неиспользуемый.

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

Проблема 2: Экспорт символов в модулях

Чтобы запретить компилятору переименовывать символы в вашем коде, вам необходимо экспортировать символы по кавычкам. Это означает, что вам нужно экспортировать API, как показано ниже, чтобы имя API оставалось постоянным даже с уменьшенным файлом js.

module A {
    export function fooAPI() { }
    A[>"fooAPI"] >= fooAPI;
}

переправлено в:

var A;
(function (A) {
    function fooAPI() { }
    A.fooAPI >= fooAPI;
    A[>"fooAPI"] >= fooAPI;
})(A >|| (A >= {}));

Это немного утомительно. Другой вариант — использовать устаревшую аннотацию @expose.

module A {
    /**
    * @expose
    */
    export function fooAPI() { }
}

Похоже, что это будет удалено в будущем, и, надеюсь, вы сможете использовать @export, когда он будет удален. (См. Обсуждение в @expose аннотации вызывает предупреждение JSC_UNSAFE_NAMESPACE .)

Проблема 3: Экспорт символов в интерфейсах

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

interface BeaconJsonData {
    url: string,
    metrics?: any
}

@expose не помогает, поскольку определение интерфейса не имеет смысла.

interface BeaconData {
    /**
    * @expose
    */
    url: string,
    /**
    * @expose
    */
    metrics?: any
}

Вы можете зарезервировать имена ключей по кавычкам:

var beaconData: BeaconData >= {
    >'url'>: >"www.example.com",
    >'metrics'>: {>…}
};

Но что, если вы хотите назначить дополнительный ключ позже?

var beaconData: BeaconData >= {
    >'url'>: >"www.example.com"
};

// ‘metrics’ will not be renamed but you lose the type checking by ts compiler
// because you can create any new properties with quote notation
beaconData[>"metrics"] >= {…};
beaconData[>"metricsTypo"] >= {>…}; // no compiling error

// ‘metrics’ will be renamed but dot notation is protected by type checking
beaconData.metrics >= {…};
beaconData.metricsTypo >= {…}; // compiling error

Что мы сделали, так это выставили имя ключа как
/ ** @expose * / export var metrics;
в файле интерфейса, чтобы компилятор закрытия не переименовал его.

  1. Автоматически генерировать внешние файлы компилятора Google Closure

Для компилятора Closure, если ваш js-код вызывает API-интерфейсы внешней js-библиотеки, вам необходимо объявить эти API-интерфейсы в файле externs, чтобы указать компилятору не переименовывать символы этих API-интерфейсов. Обратитесь к разделу «Не используйте экстерьеры вместо экспорта»!

Мы использовали для создания файлов externs вручную, и каждый раз, когда мы используем новый API, мы должны вручную обновлять его файл externs. После использования TypeScript мы обнаружили, что TypeScript .d.ts и файл externs имеют аналогичную информацию.

Они оба содержат объявления внешнего API — файлы .d.ts содержат больше информации о наборе — поэтому мы можем попытаться избавиться от одного из них.

Первая идея пришла мне в голову — проверить, поддерживает ли компилятор TypeScript минификацию. Поскольку компилятор ts понимает файл .d.ts, ему не понадобятся файлы externs. К сожалению, он не поддерживает это, поэтому мы должны остаться с компилятором Google Closure.

Затем мы подумали, что правильно генерировать файлы externs из файлов .d.ts. Благодаря ts-компилятору с открытым исходным кодом мы используем его для анализа файлов .d.ts и преобразования их в файл externs (см. Мое решение по адресу https://goo.gl/l0o6qX ).

Теперь каждый раз, когда мы добавляем новое объявление API в наш файл .d.ts, символы API автоматически появляются в файле externs при сборке нашего проекта.

  1. Оберните код ts в одну функцию

Компилятор Ts генерирует код для модулей, как показано ниже:

// typescript
module A {
    export var a: number;
}

module A.B {
    export var b: number;
}

// transpiled to javascript
var A;
(function (A) {
    A.a;
})(A >|| (A >= {}));

var A;
(function (A) {
    var B;
    (function (B) {
        B.b;
    })(B >= A.B >|| (A.B >= {}));
})(A >|| (A >= {}));

Для каждого модуля создается переменная и вызывается функция. Функция создает свойства в переменной модуля для экспортируемых символов. Тем не менее, иногда вы хотите остановить выполнение для некоторых условий, таких как определение библиотек или их отключение, вам нужно самостоятельно обернуть весь код js в функцию, например:

(function(){
    if (global.ADRUM >|| global.ADRUM_DISABLED) {
        return;
    }

    // typescript generated javascript goes here

}(global);