Расширьте свои возможности переходом с JavaScript на TypeScript

OSzone.net » Microsoft » Разработка приложений » Языки программирования » Расширьте свои возможности переходом с JavaScript на TypeScript
Автор: Билл Вагнер
Иcточник: msdn.microsoft.com
Опубликована: 04.11.2014

Язык программирования TypeScript на самом деле является надмножеством JavaScript. Если вы используете JavaScript, то уже пишете на TypeScript. Это не значит, что вы пишете хороший код на TypeScript и задействуете все его средства. Но это значит, что вы сможете плавно перевести существующую кодовую базу JavaScript в TypeScript и быстрее начать использовать преимущества новых средств TypeScript.

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

В качестве примера я начну с приложения, которое управляет адресной книгой. Это одностраничное приложение (Single-Page Application, SPA), использующее JavaScript на клиенте. Для целей этой статьи я не стану его усложнять и включу лишь ту порцию, которая отображает список контактов. Оно использует инфраструктуру Angular для связывания с данными и поддержки другой функциональности. Эта инфраструктура обрабатывает связывание с данными и создание шаблонов для отображения информации о контактах.

Приложение образуют три JavaScript-файла: app.js (содержит код, запускающий приложение), contactsController.js (контроллер страницы со списком) и contactsData.js (хранит список контактов, которые будут отображаться). Контроллер наряду с инфраструктурой Angular обрабатывает поведение страницы со списком. Вы можете сортировать контакты и показывать или скрывать более подробную информацию о любом из контактов. Файл contactsData.js содержит набор контактов, «зашитых» в код. В производственном приложении этот файл должен содержать код для вызова сервера и получения данных от него. Но «зашитый» в код список контактов лучше подходит для целей демонстрации.

Не волнуйтесь, если у вас нет особого опыта в работе с Angular. Вы сами убедитесь, насколько легко с ней работать, когда я начну перевод приложения на TypeScript. Приложение следует соглашениям Angular, которые легко сохранить при миграции на TypeScript.

Перевод приложения на TypeScript лучше всего начинать с файла контроллера. Поскольку любой допустимый код на JavaScript также является допустимым кодом на TypeScript, просто смените расширение файла контроллера contactsController с .js на .ts. TypeScript — полноправный язык в Visual Studio 2013 Update 2. Если у вас установлено расширение Web Essentials, вы увидите в одном окне как исходный код на TypeScript, так и генерируемый вывод JavaScript (рис. 1).

*

Рис. 1. Редактирование TypeScript в Visual Studio 2013 Update 2

Поскольку специфичные для TypeScript языковые средства пока не используются, эти два представления практически одинаковы. Дополнительная строка комментария в конце предоставляет информацию для Visual Studio при отладке TypeScript-приложений. Используя Visual Studio, приложение можно отлаживать на уровне TypeScript, а не на уровне сгенерированного исходного JavaScript-кода.

Как видите, компилятор TypeScript сообщает об ошибке для этого приложения, хотя компилятор генерирует допустимый JavaScript-вывод. Это одна из сильных сторон языка TypeScript. Это естественное следствие того правила, что TypeScript является надмножеством JavaScript. Я еще не объявил символ contactsApp ни в одном из TypeScript-файлов. Поэтому компилятор TypeScript предполагает тип any и считает, что данный символ будет ссылаться на объект в период выполнения. Несмотря на эти ошибки, я могу запустить приложение и оно по-прежнему будет корректно работать.

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

Для contactsApp легко объявить внешнюю переменную. По умолчанию она имеет тип any:

declare var contactsApp: any;

Хотя это устраняет ошибку при компиляции, это не помогает избежать ошибок при вызове методов в библиотеке Angular. Тип any — именно то, что подразумевает его название: это может быть что угодно. TypeScript не будет выполнять проверку типа any при доступе к переменной contactsApp. Чтобы активизировать проверку типов, нужно сообщить TypeScript о типе contactsApp и о типах, определенных в инфраструктуре Angular.

TypeScript включает информацию о типах для существующих JavaScript-библиотек с помощью такого средства, как Type Definitions. Type Definitions — это набор объявлений без реализаций. Они описывают типы и их API компилятору TypeScript. Проект DefinitelyTyped на GitHub имеет определения типов для многих популярных JavaScript-библиотек, включая Angular.js. Я включил эти определения в свой проект, используя диспетчер NuGet-пакетов.

После включения Type Definitions для библиотеки Angular я могу использовать их для устранения ошибок компиляции, которые мы уже видели. Мне нужно ссылаться на информацию о типах, только что добавленную в проект. Для этого есть специальный комментарий, который сообщает компилятору TypeScript ссылаться на информацию о типах:

/// <reference path="../Scripts/typings/angularjs/angular.d.ts" />

Теперь компилятор TypeScript может интерпретировать любые типы, определенные в файле Type Definition — angular.d.ts. Давайте исправим тип переменной contactsApp. Ожидаемый тип переменной contactsApp, объявленный в пространстве имен ng в app.js, — IModule:

declare var contactsApp: ng.IModule;

Благодаря этому объявлению я получу поддержку IntelliSense при вводе точки после contactsApp. Кроме того, я буду получать отчеты об ошибках от компилятора TypeScript всякий раз, когда буду неправильно набирать или использовать API, объявленные в объекте contactsApp. Теперь ошибки компиляции исчезают, и я включил информацию о статических типах для объекта app.

Но остальному коду в объекте contactsController по-прежнему недостает информации о типах. Пока вы не добавите аннотации типов, компилятор TypeScript будет предполагать, что тип любой переменной — any. Второй параметр в методе contactsApp.controller — это функция, и первый параметр этой функции, $scope, имеет тип ng.IScope. Поэтому я включу этот тип в объявление функции (contactData все равно будет интерпретироваться как тип any):

contactsApp.controller('ContactsController',
  function ContactsController($scope : ng.IScope, contactData) {
    $scope.sortOrder = 'last';
    $scope.hideMessage = "Hide Details";
    $scope.showMessage = "Show Details";
    $scope.contacts = contactData.getContacts();
    $scope.toggleShowDetails = function (contact) {
      contact.showDetails = !contact.showDetails;
    }
  });

Это вызывает появление нового набора ошибок компиляции. Новые ошибки возникают из-за того, что код внутри функции contactsController манипулирует свойствами, которые не являются частью типа ng.IScope. Ng.IScope — это интерфейс, и реальный объект $scope — специфичный для конкретного приложения тип, который реализует IScope. Проблемные свойства являются членами этого типа. Чтобы задействовать статическую типизацию TypeScript, я должен определить специфичный для приложения тип. Я назову его IContactsScope:

interface IContactsScope extends ng.IScope {
  sortOrder: string;
  hideMessage: string;
  showMessage: string;
  contacts: any;
  toggleShowDetails: (contact: any) => boolean;
}

Как только этот интерфейс определен, я просто изменяю тип переменной $scope в объявлении функции:

function ContactsController($scope : IContactsScope, contactData) {

После внесения этих изменений можно скомпилировать приложение без ошибок, и оно будет корректно работать. При добавлении этого интерфейса обратите внимание на несколько важных концепций. Мне не пришлось искать любой другой код и объявлять, что какой-либо конкретный тип реализует тип IContactsScope. TypeScript поддерживает структурную типизацию, собирательно называемую «утиной типизацией» (duck typing), или неявной типизацией. Это означает, что любой объект, который объявляет свойства и методы, объявленные в IContactsScope, реализует интерфейс IContactsScope независимо от того, объявлено ли в этом типе, что он реализует IContactsScope.

Заметьте, что я использую TypeScript-тип any как подстановку в определении IContactsScope. Свойство contacts представляет список контактов, а я пока не перенес тип Contact. В таком случае any можно использовать как подстановку, и компилятор TypeScript не станет выполнять проверку типа any при обращении к этим значениям. Это удобный метод при миграции приложения.

Тип any представляет любые типы, которые я пока не перенес из JavaScript в TypeScript. Это обеспечивает более плавную миграцию и уменьшает количество ошибок от компилятора TypeScript, которые приходится исправлять на каждой итерации. Я также могу искать переменные, объявленные как тип any, и тем самым узнавать, какую работу мне еще предстоит проделать. Any сообщает компилятору TypeScript не выполнять проверку типа данной переменной. Он может быть любым. Компилятор будет предполагать, что вам известны API, доступные в этой переменной. Это не значит, что каждый случай использования any плох. Существуют допустимые случаи применения типа any, например когда какой-то JavaScript API рассчитан на работу с разными типами объектов. Использование any как подстановки при миграции — один из таких полезных случаев.

Наконец, объявление toggleShowDetails показывает, как объявления функций представляются в TypeScript:

toggleShowDetails: (contact: any) => boolean;

Имя функции — toggleShowDetails. После двоеточия вы видите список параметров. Эта функция принимает единственный параметр, в настоящее время имеющий тип any. Имя contact не обязательно. Вы можете использовать его, чтобы предоставить больше информации другим программистам. Стрелка указывает возвращаемый тип — в данном примере это boolean.

Наличие типа any в определении IContactScope подсказывает вам, где надо приложить очередные усилия. TypeScript помогает избегать ошибок, когда вы даете ему больше информации о типах, с которыми вы работаете. Я заменю этот any определением того, что содержится в contact, определив тип IContact, который включает свойства, доступные в объекте contact (рис. 2).

Рис. 2. Включение свойств в объект contact

interface IContact {
  first: string;
  last: string;
  address: string;
  city: string;
  state: string;
  zipCode: number;
  cellPhone: number;
  homePhone: number;
  workPhone: number;
  showDetails: boolean
}

Определив интерфейс IContact, я буду использовать его в интерфейсе IContactScope:

interface IContactsScope extends ng.IScope {
  sortOrder: string;
  hideMessage: string;
  showMessage: string;
  contacts: IContact[];
  toggleShowDetails: (contact: IContact) => boolean;
}

Мне не нужно добавлять информацию о типах в определение функции toggleShowDetails, определенной в функции contactsController. Поскольку переменная $scope имеет тип IContactsScope, компилятор TypeScript знает, что функция, назначенная toggleShowDetails, должна соответствовать прототипу функции, определенному в IContactScope, а у параметра должен быть тип IContact.

Посмотрите на сгенерированный JavaScript для этой версии contactsController на рис. 3. Заметьте, что все определенные мной типы интерфейсов удалены из сгенерированного JavaScript. Аннотации типов существуют только для вас и для средств статического анализа. Эти аннотации не переносятся в сгенерированный JavaScript, поскольку они там не нужны.

Рис. 3. TypeScript-версия контроллера и сгенерированный JavaScript

/// reference path="../Scripts/typings/angularjs/angular.d.ts"
var contactsApp: ng.IModule;
interface IContact {
  first: string;
  last: string;
  address: string;
  city: string;
  state: string;
  zipCode: number;
  cellPhone: number;
  homePhone: number;
  workPhone: number;
  showDetails: boolean
}
interface IContactsScope extends ng.IScope {
  sortOrder: string;
  hideMessage: string;
  showMessage: string;
  contacts: IContact[];
  toggleShowDetails: (contact: IContact) => boolean;
}
contactsApp.controller('ContactsController',
  function ContactsController($scope : IContactsScope, contactData) {
    $scope.sortOrder = 'last';
    $scope.hideMessage = "Hide Details";
    $scope.showMessage = "Show Details";
    $scope.contacts = contactData.getContacts();
    $scope.toggleShowDetails = function (contact) {
      contact.showDetails = !contact.showDetails;
      return contact.showDetails;
    }
  });
// Сгенерированный JavaScript
/// reference path="../Scripts/typings/angularjs/angular.d.ts"
var contactsApp;
contactsApp.controller('ContactsController',
  function ContactsController($scope, contactData) {
  $scope.sortOrder = 'last';
  $scope.hideMessage = "Hide Details";
  $scope.showMessage = "Show Details";
  $scope.contacts = contactData.getContacts();
  $scope.toggleShowDetails = function (contact) {
    contact.showDetails = !contact.showDetails;
    return contact.showDetails;
  };
});
//# sourceMappingURL=contactsController.js.map

Добавление определений модуля и класса

Включение аннотаций типов в ваш код позволяет средствам статического анализа находить и сообщать о возможных ошибках, которые вы допустили в своем коде. Это охватывает все — от IntelliSense и анализа сомнительных синтаксических конструкций (lint-like analysis) до ошибок и предупреждений на этапе компиляции.

Еще одно важное преимущество TypeScript над JavaScript — более качественный синтаксис для определения области видимости типов. Ключевое слово module в TypeScript позволяет помещать определения типов в эту область видимости и избегать конфликтов с типами из других модулей, у которых могут быть те же имена.

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

Мое приложение-пример не настолько велико, но размещение определений типов в модулях все равно хороший стиль, помогающий избегать конфликтов. Здесь я помещу contactsController и другие определенные мной типы в модуль Rolodex:

module Rolodex {
  //Содержимое опущено
}

Я не добавил ключевое слово export ни в одно из определений в этом модуле. Это означает, что на типы, определенные в модуле Rolodex, можно ссылаться только в пределах этого модуля. Я введу ключевое слово export в интерфейсы, определенные в этом модуле, и задействую эти типы позднее, когда буду переносить код contactsData. Я также изменю код для ContactsController и превращу функцию в класс. Этому классу нужен конструктор для инициализации, но другие открытые методы не требуются (рис. 4).

Рис. 4. Преобразование ContactsController из функции в класс

export class ContactsController {
  constructor($scope: IContactsScope, contactData: any) {
    $scope.sortOrder = 'last';
    $scope.hideMessage = "Hide Details";
    $scope.showMessage = "Show Details";
    $scope.contacts = contactData.getContacts();
    $scope.toggleShowDetails = function (contact) {
      contact.showDetails = !contact.showDetails;
      return contact.showDetails;
    }
  }
}

Создание этого типа меняет вызов contactsApp.controller. Вторым параметром теперь является тип класса (class type), а не функция, определенная ранее. Первый параметр функции контроллера — это имя контроллера. Angular сопоставляет имена контроллеров с функциями-конструкторами. Везде, где в HTML-странице есть ссылки на тип ContactsController, Angular будет вызывать конструктор класса ContactsController:

contactsApp.controller('ContactsController', Rolodex.ContactsController);

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

Теперь перейдем к файлу contactData.js (рис. 5). Эта функция использует метод фабрики Angular и возвращает объект, который содержит список контактов. Как и контроллер, метод фабрики сопоставляет имена (contactData) с функцией, возвращающей сервис. Это соглашение применяется в конструкторе контроллера. Второй параметр конструктора называется contactData. Angular использует имя этого параметра для сопоставления с нужной фабрикой. Как видите, инфраструктура Angular основана на соглашениях.

Рис. 5. JavaScript-версия сервиса contactData

'use strict';
contactsApp.factory('contactData', function () {
  var contacts = [
    {
      first: "Tom",
      last: "Riddle",
      address: "66 Shack St",
      city: "Little Hangleton",
      state: "Mississippi",
      zipCode: 54565,
      cellPhone: 6543654321,
      homePhone: 4532332133,
      workPhone: 6663420666
    },
    {
      first: "Antonin",
      last: "Dolohov",
      address: "28 Kaban Ln",
      city: "Gideon",
      state: "Arkensas",
      zipCode: 98767,
      cellPhone: 4443332222,
      homePhone: 5556667777,
      workPhone: 9897876765
    },
    {
      first: "Evan",
      last: "Rosier",
      address: "28 Dominion Ave",
      city: "Notting",
      state: "New Jersey",
      zipCode: 23432,
      cellPhone: 1232343456,
      homePhone: 4432215565,
      workPhone: 3454321234
    }
  ];
  return {
    getContacts: function () {
      return contacts;
    },
    addContact: function(contact){
      contacts.push(contact);
      return contacts;
    }
  };
})

И вновь первый шаг — простая смена расширения с .js на .ts. Файл нормально компилируется, и сгенерированный JavaScript практически совпадает с исходным TypeScript. Далее я помещу код из файла contactData.ts в тот же модуль Rolodex. Это ограничит область видимости всего кода для приложения одним и тем же логическим разделом.

Затем я перенесу фабрику contactData в класс. Объявим класс как тип ContactDataServer. Вместо функции, которая возвращает объект с двумя свойствами, являющимися методами, я могу просто определить эти методы как члены объекта ContactDataServer. Начальными данным теперь является элемент данных объекта типа ContactDataServer. Мне также понадобится использовать этот тип в вызове contactsApp.factory:

contactsApp.factory('contactsData', () =>
  new Rolodex.ContactDataServer());

Второй параметр — это функция, которая возвращает новый ContactDataServer. Фабрика будет создавать этот объект, когда мне это потребуется. Если я попытаюсь скомпилировать и запустить эту версию, я получу ошибки компиляции, потому что тип ContactDataServer не экспортируется из модуля Rolodex. Однако на него есть ссылка в вызове contactsApp.factory. Я могу легко исправить эту ошибку, добавив ключевое слово export в объявление класса ContactDataServer.

Окончательная версия показана на рис. 6. Заметьте, что я добавил информацию о типах для массива контактов и входного параметра в методе addContact. Аннотации типов не обязательны — код на TypeScript допустим и без них. Но я все же советую добавлять всю необходимую информацию о типах в ваш код на TypeScript, потому что это помогает избегать ошибок в системе типов TypeScript.

Рис. 6. TypeScript-версия ContactDataServer

/// reference path="../Scripts/typings/angularjs/angular.d.ts"
var contactsApp: ng.IModule;
module Rolodex {
  export class ContactDataServer {
    contacts: IContact[] = [
      {
        first: "Tom",
        last: "Riddle",
        address: "66 Shack St",
        city: "Little Hangleton",
        state: "Mississippi",
        zipCode: 54565,
        cellPhone: 6543654321,
        homePhone: 4532332133,
        workPhone: 6663420666,
        showDetails: true
      },
      {
        first: "Antonin",
        last: "Dolohov",
        address: "28 Kaban Ln",
        city: "Gideon",
        state: "Arkensas",
        zipCode: 98767,
        cellPhone: 4443332222,
        homePhone: 5556667777,
        workPhone: 9897876765,
        showDetails: true
      },
      {
        first: "Evan",
        last: "Rosier",
        address: "28 Dominion Ave",
        city: "Notting",
        state: "New Jersey",
        zipCode: 23432,
        cellPhone: 1232343456,
        homePhone: 4432215565,
        workPhone: 3454321234,
        showDetails: true
      }
    ];
    getContacts() {
      return this.contacts;
    }
    addContact(contact: IContact) {
      this.contacts.push(contact);
      return this.contacts;
    }
  }
}
contactsApp.factory('contactsData', () =>
  new Rolodex.ContactDataServer());

Теперь, когда я создал новый класс ContactDataServer, я могу внести последнее изменение в контроллер. Вспомните: второй параметр конструктора contactsController был сервером данных. Сейчас это можно сделать более безопасным способом, объявив, что следующий параметр должен быть типа ContactDataServer:

constructor($scope: IContactsScope, contactData: ContactDataServer) {

Плавный переход с JavaScript на TypeScript

В TypeScript гораздо больше средств, чем было продемонстрировано здесь. По мере работы с TypeScript вы изучите эти средства. Чем чаще вы будете пользоваться TypeScript-расширениями JavaScript, тем больше повысится производительность вашего труда. Помните, что аннотации типов TypeScript предназначены для плавного перехода с JavaScript на TypeScript. И самое важное: учитывайте, что TypeScript — это надмножество JavaScript. То есть любой допустимый JavaScript-код является допустимым TypeScript-кодом.

Кроме того, аннотации типов TypeScript проверяются только там, где вы их предоставляете, и вы не обязаны добавлять их повсюду. Еще раз повторюсь, что они очень полезны при переходе с JavaScript на TypeScript.

Наконец, система типов TypeScript поддерживает структурную типизацию. Когда вы определяете интерфейсы для важных типов, система типов TypeScript будет предполагать, что любой объект с этими методами и свойствами поддерживает данный интерфейс. От вас не требуется объявлять поддержку интерфейса в определении каждого класса. Анонимные объекты тоже могут поддерживать интерфейсы, используя структурную типизацию.

Сочетание всех этих средств обеспечивает плавный перевод вашей кодовой базы с JavaScript под TypeScript. Чем дальше вы продвигаетесь по пути миграции, тем больше преимуществ вы получаете от статического анализа кода в TypeScript. Вашей конечной целью должно стать использование как можно большей надежности TypeScript. Попутно существующий у вас JavaScript-код будет функционировать как допустимый TypeScript-код, не использующий аннотации типов TypeScript. Этот процесс проходит почти без трения. У вас нет никаких причин на то, чтобы не использовать TypeScript в своих текущих JavaScript-приложениях.


Ссылка: http://www.oszone.net/25399/