Этот сайт использует файлы cookies. Оставаясь на сайте, Вы соглашаетесь с использованием файлов cookies и принимаете Соглашение об использовании сайта.

язык TypeScript

TypeScript для приложений Angular

1405
1405

Введение

Язык JavaScript является очень гибким с точки зрения манипулирования данными. Он позволяет практически в любой объект добавить (или удалить) любое поле или метод произвольного типа. Такая неограниченная свобода дает некоторые преимущества разработчикам при решении задач. В то же время она возлагает на разработчика ответственность за контроль типов объектов, их полей, параметров методов и т. п.

Упростить эту задачу для разработчиков призван язык TypeScript. Он привносит в JavaScript контроль типов, очень похожий на тот, что применяется в строго типизированных языках, таких как C#. TypeScript позволяет использовать некоторые возможности нового стандарта ES6.

Эта статья не предполагает полное описание всех возможностей TypeScript. Подробно ознакомиться с ними можно на официальном сайте typescriptlang.org.

Речь пойдет о применении TypeScript для приложений, использующих Angular. Для остальных случаев могут быть отличительные особенности использования TypeScript, не рассмотренные в данной статье.

Внедрение

Перейти на использование языка TypeScript в проекте достаточно просто, если вы используете Visual Studio для разработки вашего проекта. Если у вас установлено расширение TypeScript для Visual Studio, то компиляция файлов TypeScript производится самой программой и не требует каких-либо дополнительных усилий.

type script
Окно с расширением TypeScript для Visual Studio.

Если же вы не используете Visual Studio, то компиляция файлов TypeScript может быть настроена с помощью Gulp, Grunt и т. п.

var gulp = require('gulp');
var ts = require('gulp-typescript');
var transpilerDest = 'Scripts/app';
var transpilerConfig = 'Scripts/tsconfig.json';

var tsProject = ts.createProject(transpilerConfig);

gulp.task('ts', function () {
    var tsResult = tsProject.src()
        .pipe(ts(tsProject));

    return tsResult.js.pipe(gulp.dest(transpilerDest));
});

TypeScript полностью обратно совместим с JavaScript, поэтому переход на TypeScript происходит довольно просто. Нет необходимости за раз переделывать все файлы js в ts. Можно переделывать их по одному по мере возможности, не теряя при этом работоспособности проекта.

Конфигурация

Для настройки компиляции файлов TypeScript используется файл «tsconfig.json». С полным списком настроек можно ознакомиться в документации, однако большинство настроек имеет вполне подходящее значение по умолчанию, так что специально их устанавливать нет необходимости. Здесь хотелось упомянуть те, которые мы использовали:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "noEmitOnError": true,
    "removeComments": false,
    "sourceMap": true,
    "target": "es5"
  },
  "exclude": [
    "excludedFile.ts",
    "excluded_folder"
  ],
  "compileOnSave": true
}

1. "compilerOptions"содержит настройки для компиляции файлов ts:

"noImplicitAny" — выдает ошибку в том случае, если тип переменной или параметра явно не указан. На начальном этапе перехода с JavaScript на TypeScript можно эту опцию устанавливать в значение false.

В этом случае все переменные и параметры, тип которых не объявлен, будут считаться неявно имеющими тип any, т. е. произвольный. И файлы ts будут компилироваться без ошибок. Однако в таком случае контроль типов TypeScript частично утрачивается, поэтому в дальнейшем, после того, как все нужные типы будут определены и указаны, рекомендуется установить значение этой опции в true.

"noEmitOnError" — не создает файл js, если в файле ts обнаружена ошибка. Для более строгого контроля правильности исходного кода рекомендуется устанавливать эту опцию в true.

"removeComments" — удаляет комментарии в ts файлах при генерации файлов js. Значение данной опции, пожалуй, не имеет принципиального значения, и каждый разработчик может устанавливать так, как ему будет удобней.

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

"sourceMap" — генерирует «.map» файлы вместе с js файлами. Они используются для отладки исходного кода в браузере. Об отладке мы поговорим чуть позже.

"target" — определяет целевой стандарт JavaScript для компиляции. Если в исходном коде используются некоторые новые возможности ES6, то при компиляции под ES5 они преобразуются таким образом, чтобы соответствовать старому стандарту ES5, сохранив свою функциональность.

В дальнейшем, когда стандарт ES6 будет широко распространен и более полно поддерживаться подавляющим большинством браузеров, целевой стандарт можно будет заменить на ES6. В этом случае нововведения из ES6 при компиляции ts файла будут переходить в js практически в неизменном виде.

2. "exclude" — позволяет исключить из процесса компиляции ненужные файлы или папки. Данная опция показалась нам удобнее, чем опция "files", которая требует указания полного списка файлов, которые требуется откомпилировать, так как избавляет нас от необходимости каждый раз при добавлении нового ts файла в проект редактировать файл конфигурации «tsconfig.json».

3. "compileOnSave" — опция для Visual Studio, запускающая процесс компиляции ts файла при его сохранении. Данная опция достаточно удобна в использовании, однако иногда возникала ситуация, когда при сохранении ts файла js файл не обновлялся. Очистка и повторная сборка проекта позволяли решить эту проблему. Поэтому приходилось иногда проверять обновился ли js файл и соответствует ли он измененному ts файлу, особенно в случае возникновения проблем при тестировании приложения.

Отладка

Если при компиляции TypeScript кода были сгенерированы «.map» файлы, то в браузере можно производить отладку непосредственно ts кода: устанавливать точки останова, просматривать значения переменных и пр., аналогично отладке непосредственно js кода. Однако есть одна особенность при отладке кода стрелочных функций (=>).

Стрелочная функция сохраняет значение указателя this из своего внешнего окружения. Если в качестве целевого стандарта JavaScript выбран ES5, то браузер показывает, что значением this является undefined, при этом сам код работает нормально, т. е. обращение к полям и методам this не вызывает никаких ошибок. Это связано с тем, что стандарт ES5 не поддерживает стрелочные функции, поэтому они при компиляции заменяются на обычные функции. При этом указатель this сохраняется в локальной переменной:

var _this = this;

Затем именно он используется в коде стрелочной функции. Поэтому если вам необходимо посмотреть, чем на самом деле является this или посмотреть значения некоторых его свойств, то можно поискать в замыкании функции переменную _this, т. к. именно она используется вместо реального this.

Файлы объявлений

Если некоторые типы данных используются повсеместно в проекте, то объявления данных типов можно вынести в отдельный файл, называемый файлом объявлений. Название файла имеет следующий формат: «fileName.d.ts». В файле объявлений мы определяем пространство имен, внутри которого объявляем нужные нам типы и интерфейсы. Для интерфейсов нет необходимости использовать ключевое слово «export», поскольку они и так будут доступны.

declare namespace MyApp {

    interface IMyInterface {
        name: string;
        count: number;
        valid: boolean;
    }

}

Основная особенность файлов объявлений заключается в том, что для них не создается соответствующий js файл.

Библиотеки типов

Для всех переменных, функций и их параметров в нашем проекте нам необходимо указать их типы. А как же быть со сторонними библиотеками? К счастью, для нас есть сайт definitelytyped.org — репозиторий файлов объявлений типов для большинства наиболее популярных библиотек JavaScript.

Существуют различные способы установки компонентов DefinitelyTyped в проект. В нашем проекте мы решили использовать пакеты NuGet, поскольку NuGet уже применялся для подключения библиотек C#.

Итак, первым делом, нам необходимо подключить объявления типов для самого Angular. Данный компонент выглядит следующим образом:

type script
Компонент подключения объявления типов для Angular.

Найдите объявления типов для всех остальных библиотек, используемых на проекте. А если объявления типов для нужной библиотеки не найдены в репозитории?

В этом случае вам нужно самостоятельно написать объявления типов для этой библиотеки. Нет никакой необходимости описывать интерфейс библиотеки полностью. Достаточно описать только те API, которые используются в проекте. Это позволит значительно сэкономить рабочее время. К счастью, для большинства сложных и популярных библиотек описания типов уже существуют. И вам придется дописывать описания только для весьма небольших и несложных библиотек, имеющих довольно простой API.

Модули

TypeScript поддерживает несколько систем модулей, используемых разработчиками JavaScript, среди которых Сommon JS, System JS, AMD, UMD, ES6. Но поскольку Angular имеет свою собственную систему модулей, то нам не было необходимости использовать модули TypeScript. Все же, чтоб наши объявления не находились в глобальном пространстве имен, во избежание возможных конфликтов мы использовали один единственный модуль TypeScript для всего нашего приложения. Это позволило упростить доступ к определяемым в приложении интерфейсам, т. к. избавляет от необходимости указывать пространство имен при обращении к интерфейсам.

В скомпилированном файле JavaScript модуль представляет собой обычный объект JavaScript, в который добавляются все экспортируемые классы, переменные, функции модуля. Поскольку мы использовали систему модулей Angular, то мы принципиально не экспортировали из нашего модуля никаких классов, переменных или функций, только интерфейсы.

При компиляции файла ts в js вся информация о типах просто удаляется, ведь она не используется браузерами при исполнении кода, а используется только для контроля правильности написания кода на этапе компиляции. Для экспортируемых из модуля интерфейсов никакой js-код не создается. Поэтому наш модуль по сути представляет собой пустой объект JavaScript. Конфликты переменных, классов или функций в нашем модуле TypeScript невозможны.

Сервисы

Если при написании приложения с использованием Angular на JavaScript гораздо проще использовать фабрики, то на TypeScript гораздо проще реализовывать сервисы. Поэтому при переходе на TypeScript большинство фабрик были заменены на сервисы.

TypeScript поддерживает классы, аналогичные классам ES6. Эти классы как нельзя лучше подходят для тех методов Angular, которые принимают в качестве параметров функцию-конструктор, а именно сервис и контроллер.

Для реализации сервиса мы предварительно объявляем интерфейс для сервиса, который экспортируем. Затем создаем класс для сервиса, реализующий интерфейс сервиса. И далее вызываем метод Angular для добавления сервиса.

 module MyApp {

    "use strict";

    export interface IMyService {
        myMethod(param: string);
    }

    class MyService implements IMyService {

        static $inject = ["Dependency"];

        constructor(private dependency: IDependency) {
        }

        myMethod(param: string) {
            return this.dependency.field + param;
        }

    }

    angular.module("services").service("MyService", MyService);

}
  

Параметры конструктора, объявленные с модификаторами private или public, становятся полями класса и получают значения, соответствующие значениям параметров конструктора.

Для указания зависимостей (параметров) функции конструктора очень удобно использовать статическое поле $inject, которому присваивается строковый массив, содержащий имена зависимостей.

Класс сервиса не экспортируется в модуле, а значит он не доступен в других файлах проекта. Поэтому в файлах, использующих наш сервис, мы используем объявленный нами интерфейс сервиса для объявления типа параметра.

Контроллеры

Простой пример реализации контроллера на TypeScript, использующего в качестве зависимости наш сервис из предыдущего примера:

 module MyApp {

    "use strict";

    class MyController {

        static $inject = ["MyService"];

        constructor(private myService: IMyService) {
        }

        get stringField() {
            return this.myService.("some string");
        }

    }

    angular.module("pages").controller("MyController", MyController);
}  

Для контроллеров нет необходимости объявлять интерфейс, поскольку они как правило не используются в качестве зависимостей у других компонентов.

Константы

Константы реализуются аналогично сервисам, с той лишь разницей, что вместо класса константы в качестве аргумента, мы передаем вновь созданный объект нашего класса. Реализация классом константы интерфейса гарантирует нам, что в классе константы присутствуют все поля (и методы), объявленные в интерфейсе константы. При этом константы не могут иметь никаких зависимостей.

 module MyApp {

    "use strict";

    export interface IDateFormats {
        dateFormat: string;
        timeFormat: string;
    }

    class DateFormats implements IDateFormats {
        dateFormat = "MM/dd/yyyy";
        timeFormat = "HH:mm:ss";
    }

    angular.module("constants").constant("DateFormats", new DateFormats());

}  

Для прочих методов Angular, требующих в качестве параметра функцию, использование классов TypeScript не рекомендуется. Это может привести к путанице и ошибкам, поскольку такие функции вызываются без установленного указателя this.

Компоненты

Фрагменты однотипной разметки страниц удобно объединять в компоненты. Это позволяет упростить разметку страницы, повторно использовать один и тот же код, облегчает доработку и усовершенствование. Достаточно подправить лишь код или шаблон одного компонента, и все страницы, на которых он используется, обновятся соответственно.

Компоненты можно реализовывать следующим образом:

module MyApp {

    "use strict";

    class MyComponentController {

        title: string;
        data: IMyData;
        event: () => void;

        constructor() {
            …
        }

        someMethod() {
            this.event();
        }

    }

    const options: ng.IComponentOptions = {
        bindings: {
            title: "@",
            data: "=",
            event: "&"
        },
        controller: MyComponentController,
        templateUrl: "myComponent.html"
    };

    angular.module("components").component("MyComponent", options);

}  

Поля класса, используемые в «bindings», доступны уже на этапе вызова конструктора класса.

События

Если нам необходимо, чтобы метод класса вызывался при возникновении какого-либо события, то, как правило, мы не должны в качестве обработчика события использовать непосредственно сам метод класса, т. к. при вызове обработчика события ему не будет передаваться корректный указатель на this. А метод класса может ссылаться на поля класса или вызывать другие методы класса.

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

service.setEventHandler(() => this.eventHandler());

В данном примере при вызове eventHandler мы будем иметь корректный указатель this.

Заключение

Контроль типов — это именно то, чего не хватает JavaScript для облегчения написания правильного и работоспособного кода. И этот контроль типов привносит TypeScript. Если у вас уже есть опыт разработки на одном из строго типизированных языков, вроде C# или Java, а также если вы владеете языком JavaScript и знакомы с его новым стандартом ES6, то освоить TypeScript для вас не составит труда.

На начальном этапе переход на TypeScript немного трудоемок, поскольку необходимо описывать типы используемых классов, переменных, функций. Но в дальнейшем он значительно упрощает разработку. Так, при изменении типа или количества параметров какого-либо метода, компилятор TypeScript сразу же подскажет нам, где необходимо переделать вызовы этого метода. К тому же TypeScript позволяет использовать некоторые нововведения ES6 при создании кода для предыдущего стандарта ES5. Возможность одновременного использования как TypeScript кода, так и JavaScript кода, позволяет поэтапно внедрять TypeScript в уже существующие проекты.

В нашем проекте используется Angular 1.x для front-end разработки. Это действительно очень удобный, мощный и гибкий фреймворк. Он значительно упрощает отображение различных данных, а также их изменение. Помимо этого, он содержит массу дополнительных сервисных функций. Однако такое обилие и разнообразие API создает некоторые трудности в их освоении и применении, поскольку необходимо периодически сверяться с документацией Angular, чтобы уточнить, какие именно доступны функции и методы, какие им необходимы аргументы и какие именно значения они возвращают. И здесь нам TypeScript оказывает неоценимую помощь.

Система IntelliSense показывает нам, какие методы и свойства доступны у того или иного объекта, каков тип свойства, какие типы аргументов нужны для того или иного метода, а также какие типы возвращают эти методы. Нам доступны справочные описания методов и их параметров, что в ряде случаев снимает необходимость обращения к документации Angular даже на начальном этапе изучения и освоения фреймворка. Это в полной мере относится и к другим библиотекам, для которых доступны файлы описания DefinitelyTyped.

Наличие описаний методов, их аргументов и возвращаемых значений упростило и ускорило процесс разработки.

Помимо этого, контроль типов TypeScript позволяет нам выявить некоторые ошибки в коде уже на этапе компиляции. Тогда как при использовании JavaScript подобные ошибки обнаруживаются только во время выполнения кода. И это также ускоряет процесс разработки.

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

TypeScript позволяет писать более красивый, компактный, более понятный и легко поддерживаемый код. К тому же переход на новый стандарт JavaScript ES6 в будущем не составит никакого труда. Поэтому хотелось бы всем порекомендовать использовать TypeScript в своих проектах.