Несомненно, что у вас накопились колоссальные вложения в Microsoft .NET Framework, и это действительно фундаментальная платформа с богатым инструментарием. Если вы владеете C# или Visual Basic .NET в сочетании с XAML, рынок приложения ваших существующих знаний может показаться почти безграничным. Однако сегодня вам нужно принимать во внимание язык, который в свое время был общепризнанным, но в последние несколько лет фактически был оторван от этой платформы, а сейчас возвращается. Я говорю о JavaScript, конечно. Рост количества приложений JavaScript и их возможностей огромен. Node.js — полноценная платформа для разработки масштабируемых приложений JavaScript — стала чрезвычайно популярной, и ее можно развертывать даже в Windows Azure. Более того, JavaScript можно использовать совместно с HTML5 для разработки игр, мобильных приложений и теперь приложений Windows Store.
.NET-разработчик не может игнорировать ни возможности JavaScript, ни его распространенность на рынке. Когда я говорю это своим коллегам, я часто слышу в ответ ворчание, что с JavaScript трудно работать, в нем нет строгой типизации, нет структур классов. Я отметаю эти доводы, отвечая, что JavaScript является функциональным языком и что существуют шаблоны, реализующие необходимые им вещи.
И вот здесь в игру вступает TypeScript. Это не новый язык. Это надмножество JavaScript мощное и типизированное, а значит, весь JavaScript-код является допустимым TypeScript-кодом, и компилятор генерирует JavaScript-код. TypeScript — проект с открытым исходным кодом, всю информацию о нем можно найти на typescriptlang.org. На момент написания этой статьи TypeScript был в виде предварительной версии 0.8.1.
В этой статье я расскажу о базовых концепциях TypeScript в форме классов, модулей и типов, чтобы показать, насколько .NET-разработчик станет комфортнее чувствовать себя в JavaScript-проекте.
Классы
Если вы работаете с такими языками, как C# или Visual Basic .NET, вам хорошо известна концепция классов. В JavaScript классы и наследование реализуются на основе шаблонов вроде замыканий (closures) и прототипов. TypeScript вводит классический синтаксис типов, к которому вы привыкли, а компилятор генерирует JavaScript, реализующий это намерение (intent). Возьмем следующий фрагмент JavaScript-кода:
var car;
car.wheels = 4;
car.doors = 4;
Он кажется простым и прямолинейным. Однако .NET-разработчики неохотно брались за JavaScript из-за его свободного обращения с определениями объектов. Объект car может получить дополнительные свойства, не зная их типы данных, и в итоге в период выполнения будут генерироваться исключения. Как определение модели классов в TypeScript меняет эту ситуацию и как осуществляется наследование и расширение объекта car? Рассмотрим пример в табл. 1.
Табл. 1. Объекты в TypeScript и JavaScript
TypeScript | JavaScript |
class Auto{ wheels; doors; } var car = new Auto(); car.wheels = 2; car.doors = 4; | var Auto = (function () { function Auto() { } return Auto; })(); var car = new Auto(); car.wheels = 2; car.doors = 4; |
Слева вы видите прекрасно определенный объект класса car со свойствами wheels и doors, а справа — JavaScript-код, сгенерированный компилятором TypeScript, и он почти такой же. Единственное различие — переменная Auto.
В редакторе TypeScript нельзя добавить дополнительное свойство без получения предупреждения. Вы просто не сможете начать с использования выражения вроде car.trunk = 1. Компилятор пожалуется: «No trunk property exists on Auto» (в Auto нет свойства trunk), и это просто манна небесная для любого, кому хоть раз попадал в такую ловушку из-за гибкости JavaScript (или его «лени» — в зависимости от вашей точки зрения на JavaScript).
Конструкторы, тоже доступные в JavaScript, опять же усовершенствованы в TypeScript за счет отслеживания создания объекта на этапе компиляции: объект не может быть создан без передачи в вызове соответствующих элементов и типов.
Вы можете не только добавить конструктор к классу, но и сделать параметры необязательными, задать значение по умолчанию или сократить объявление свойства. Посмотрим на три примера, иллюстрирующих, насколько эффективным может быть TypeScript.
Первый пример приведен в табл. 2: простой конструктор, в котором класс инициализируется передачей параметров wheels и doors (представленных здесь переменными w и d). Генерируемый JavaScript (справа) почти эквивалентен, но по мере расширения динамики и потребностей вашего приложения это не всегда будет так.
Табл. 2. Простой конструктор
TypeScript | JavaScript |
class Auto{ wheels; doors; constructor(w, d){ this.wheels = w; this.doors = d; } } var car = new Auto(2, 4); | var Auto = (function () { function Auto(w, d) { this.wheels = w; this.doors = d; } return Auto; })(); var car = new Auto(2, 4); |
В табл. 3 я модифицировал код из табл. 2, присвоив параметру wheels (w) значение по умолчанию (4) и сделав параметр doors (d) необязательным, вставив за ним знак вопроса. Заметьте, что, как и в предыдущем примере, шаблон присваивания аргументов свойствам экземпляра— распространенная практика, при которой используется ключевое слово this.
Табл. 3. Простой конструктор, модифицированный
TypeScript | JavaScript |
class Auto{ wheels; doors; constructor(w = 4, d?){ this.wheels = w; this.doors = d; } } var car = new Auto(); | var Auto = (function () { function Auto(w, d) { if (typeof w === "undefined") { w = 4; } this.wheels = w; this.doors = d; } return Auto; })(); var car = new Auto(4, 2); |
Здесь есть возможность, которую я бы очень хотел увидеть в .NET-языках: чтобы объявить какое-либо свойство в классе, можно просто добавить ключевое слово public перед именем параметра в конструкторе. Также доступно ключевое слово private, и оно реализует то же автоматическое объявление, но скрывает это свойство класса.
Значения по умолчанию, необязательные параметры и аннотации типов расширяются с помощью функциональности автоматического объявления свойства в TypeScript, что сокращает набираемый вами текст и повышает производительность вашего труда. Сравните скрипт в табл. 4 и вы увидите, как на поверхность начинают всплывать отличия в уровне сложности.
Табл. 4. Возможность автоматического объявления
TypeScript | JavaScript |
class Auto{ constructor(public wheels = 4, public doors?){ } } var car = new Auto(); car.doors = 2; | var Auto = (function () { function Auto(wheels, doors) { if (typeof wheels === "undefined") { wheels = 4; } this.wheels = wheels; this.doors = doors; } return Auto; })(); var car = new Auto(); car.doors = 2; |
Классы в TypeScript также поддерживают наследование. Придерживаясь примера с Auto, вы можете создать класс Motorcycle, который расширяет этот начальный класс. На рис. 1 я добавляю в базовый класс функции drive и stop. Добавление класса Motorcycle (который наследует от Auto и задает соответствующие свойства для doors и wheels) осуществляется несколькими строками кода на TypeScript.
Рис. 1. Добавление класса Motorcycle
class Auto{
constructor(public mph = 0,
public wheels = 4,
public doors?){
}
drive(speed){
this.mph += speed;
}
stop(){
this.mph = 0;
}
}
class Motorcycle extends Auto
{
doors = 0;
wheels = 2;
}
var bike = new Motorcycle();
Здесь нужно запомнить важную вещь: в начале сгенерированного JavaScript вы видите небольшую функцию ___extends (рис. 2), которая представляет собой единственный блок кода, вставленный в конечный JavaScript. Это вспомогательный класс, помогающий в наследовании. В качестве отступления замечу, что у этой функции одна и та же сигнатура независимо от исходного кода, поэтому, если вы распределяет свой JavaScript-код по нескольким файлам и используете такую утилиту, как SquishIt или Web Essentials, для комбинирования скриптов, то можете получить ошибку в зависимости от того, как утилита корректирует дублируемые функции.
Рис. 2. JavaScript-код, сгенерированный компилятором
var __extends = this.__extends || function (d, b) {
function __() { this.constructor = d; }
__.prototype = b.prototype;
d.prototype = new __();
}
var Auto = (function () {
function Auto(mph, wheels, doors) {
if (typeof mph === "undefined") { mph = 0; }
if (typeof wheels === "undefined") { wheels = 4; }
this.mph = mph;
this.wheels = wheels;
this.doors = doors;
}
Auto.prototype.drive = function (speed) {
this.mph += speed;
};
Auto.prototype.stop = function () {
this.mph = 0;
};
return Auto;
})();
var Motorcycle = (function (_super) {
__extends(Motorcycle, _super);
function Motorcycle() {
_super.apply(this, arguments);
this.doors = 0;
this.wheels = 2;
}
return Motorcycle;
})(Auto);
var bike = new Motorcycle();
Модули
Модули в TypeScript эквивалентны пространствам имен в .NET Framework. Это отличный способ организации вашего кода, инкапсуляции бизнес-правил и обработки, которые были бы невозможны без этой функциональности (в JavaScript нет встроенного способа предоставлений этой функции). Шаблон модуля (или динамическое создание пространств имен как в JQuery) — самый распространенный шаблон для пространств имен в JavaScript. Модули TypeScript упрощают синтаксис и дают тот же результат. В примере с Auto вы можете обернуть код в модуль и предоставлять доступ только к классу Motorcycle, как показано на рис. 3.
Рис. 3. Обертывание класса Auto в модуль
module Example {
class Auto{
constructor(public mph : number = 0,
public wheels = 4,
public doors?){
}
drive(speed){
this.mph += speed;
}
stop(){
this.mph = 0;
}
}
export class Motorcycle extends Auto
{
doors = 0;
wheels = 2;
}
}
var bike = new Example.Motorcycle();
Модуль Example инкапсулирует базовый класс, и класс Motorcycle предоставляется за счет включения в него префикса с ключевым словом export. Это позволяет создавать экземпляр Motorcycle и использовать все его методы, но базовый класс Auto остается скрытым.
Другое важное преимущество модулей в том, что вы можете объединять их. Если создать еще один модуль под тем же именем — Example, то TypeScript предполагает, что код в первом и новом модуле доступен через выражения Example (точно так же, как в пространствах имен).
Модули облегчают сопровождение и структуризацию кода. С их помощью поддержка крупномасштабных приложений становится менее обременительной для групп разработчиков.
Типы
Отсутствие безопасности типов (type safety) — одна из наиболее частых жалоб, которые я слышу от тех, кто не работает с JavaScript на постоянной основе. Но безопасность типов доступна в TypeScript (вот почему он так называется) и выходит далеко за рамки простого объявления переменной как строковой или булевой.
В JavaScript практика присваивания x = foo с последующим присваиванием в коде x = 11 является совершенно допустимой, но она может свести с ума, когда пытаешься разобраться, почему при запуске программы неизменно получаешь NaN.
Безопасность типов — одно из крупнейших преимуществ TypeScript, и в нем имеются четыре встроенных типа: string, number, bool и any. На рис. 4 показаны синтаксис объявления типа переменной s и работа IntelliSense, предоставляемую компилятор после того, как ему становится известно, какие действия вы можете выполнять на основе этого типа.
Рис. 4. Пример IntelliSense в TypeScript
Помимо типизации переменной или функции, TypeScript умеет логически распознавать типы. Вы можете создать функцию, которая просто возвращает строку. Зная это, компилятор и прочие утилиты обеспечивают логическое распознавание типа (type inference) и автоматически показывают операции, которые можно выполнять над возвращаемым значением (рис. 5).
Рис. 5. Пример логического распознавания типов
Здесь преимущество в том, что вы видите, что возвращается строка и вам не приходится строить никаких догадок. Логическое распознавание типов здорово помогает при работе с другими библиотеками, на которые есть ссылки в коде, например с JQuery или даже с Document Object Model (DOM).
Еще один способ задействовать преимущества системы типы — использование аннотаций. Если вернуться к началу статьи, то исходный класс Auto был объявлен только со свойствами wheels и doors. Теперь за счет аннотаций мы можем гарантировать, что при создании экземпляра Auto в car будут присваиваться должные типы:
class Auto{
wheels : number;
doors : number;
}
var car = new Auto();
car.doors = 4;
car.wheels = 4;
Однако в генерируемом JavaScript компилятор убирает эти аннотации, поэтому в коде не появляется лишних зависимостей. И вновь преимущество заключается в строгой типизации и исключении простых ошибок, которые обычно обнаруживаются только при выполнении.
Интерфейсы — другой пример безопасности типов, обеспечиваемой TypeScript. Интерфейсы позволяют вам определять форму объекта. На рис. 6 в класс Auto добавлен новый метод travel, принимающий параметр с типом Trip.
Рис. 6. Интерфейс Trip
interface Trip{
destination : string;
when: any;
}
class Auto{
wheels : number;
doors : number;
travel(t : Trip) {
//..
}
}
var car = new Auto();
car.doors = 4;
car.wheels = 4;
car.travel({destination: "anywhere", when: "now"});
Если вы попытаетесь вызвать метод travel с некорректной структурой, компилятор этапа разработки сообщит об ошибке. Для сравнения, если вы наберете такой же код в JavaScript, поместив его, скажем, в файл .js, то скорее всего не отловите эту ошибку до запуска приложения.
На рис. 7 показано, насколько полезно использование аннотаций типов, причем не только для разработчика-автора, но и для другого разработчика, которому в будущем придется сопровождать этот код.
Рис. 7. Аннотации помогают в сопровождении кода
Существующий код и библиотеки
Так что же будет с вашим существующим кодом на JavaScript или как быть, если вы предпочитаете программировать поверх Node.js или такие библиотеки, как toastr, Knockout или JQuery? В TypeScript есть файлы объявлений (declaration files), которые помогут в этом. Прежде всего вспомните, что любой код на JavaScript является допустимым TypeScript. Поэтому, если у вас есть собственные наработки, вы можете скопировать этот код прямо в дизайнер, и компилятор сгенерирует JavaScript-код «один в один». Вариант получше — создать свой файл объявлений.
Для основных библиотек и инфраструктур Борис Янков (Boris Yankov) (twitter.com/borisyankov) создал отличный репозитарий на GitHub (github.com/borisyankov/DefinitelyTyped), где хранится набор файлов объявлений для некоторых из наиболее популярных библиотек JavaScript. Как раз на это очень рассчитывала группа TypeScript. Кстати, файл объявлений для Node.js был создан самой группой TypeScript, и он доступен как часть исходного кода.
Создание файла объявлений
Если вы не можете найти файл объявлений для своей библиотеки или если вы работаете со своим кодом, вам нужно создать файл объявлений. Начните с копирования JavaScript в TypeScript и добавления определений типов, а затем используйте утилиту командной строки для генерации файла объявлений (с расширением .d.ts), на который вы будете ссылаться.
В табл. 5 показан простой скрипт для вычисления среднего балла на JavaScript. Я скопировал этот скрипт в левую панель редактора и добавил аннотации для типов, а потом сохранил файл с расширением .ts.
Табл. 5. Создание файла объявлений
TypeScript | JavaScript |
function gradeAverage(grades : string[]) { var total = 0; var g = null; var i = -1; for(i = 0; i < grades.length; i++) { g = grades[i]; total += getPointEquiv(grades[i]); } var avg = total / grades.length; return getLetterGrade(Math.round(avg)); } function getPointEquiv(grade : string) { var res; switch(grade) { case "A": { res = 4; break; } case "B": { res = 3; break; } case "C": { res = 2; break; } case "D": { res = 1; break; } case "F": { res = 0; break; } } return res; } function getLetterGrade(score : number) { if(score < 1) { return "F"; } if(score > 3) { return "A"; } if(score > 2 && score < 4) { return "B"; } if(score >= 1 && score <= 2) { return "C"; } if(score > 0 && score < 2) { return "D"; } } | function gradeAverage(grades){ var total = 0; var g = null; var i = -1; for(i = 0; i < grades.length; i++) { g = grades[i]; total += getPointEquiv(grades[i]); } var avg = total / grades.length; return getLetterGrade(Math.round(avg)); } function getPointEquiv(grade) { var res; switch(grade) { case "A": { res = 4; break; } case "B": { res = 3; break; } case "C": { res = 2; break; } case "D": { res = 1; break; } case "F": { res = 0; break; } } return res; } function getLetterGrade(score) { if(score < 1) { return "F"; } if(score > 3) { return "A"; } if(score > 2 && score < 4) { return "B"; } if(score >= 1 && score <= 2) { return "C"; } if(score > 0 && score < 2) { return "D"; } } |
Далее я открыл окно командной строки и воспользовался утилитой командной строки TypeScript для создания файла определений и конечного JavaScript:
tsc c:\gradeAverage.ts –declarations
Компилятор создает два файла: gradeAverage.d.ts (файл объявлений) и gradeAverage.js (JavaScript-файл). Теперь в любом будущем TypeScript-файле, где потребуется функционал gradeAverage, я просто добавлю ссылку в его начало в редакторе:
/// <reference path="gradeAverage.d.ts">
После этого весь текст и инструментарий, ссылающийся на эту библиотеку, выделяются цветом, и так происходит с любой из основных библиотек, которые можно найти в репозитарии DefinitelyTyped GitHub.
Отличный функционал, предоставляемый компилятором для файлов объявлений, — возможность автоматического прохождения по ссылкам. На практике это означает вот что. Если вы ссылаетесь на файл объявлений для jQueryUI, который в свою очередь ссылается на jQuery, ваш текущий файл на TypeScript получит поддержку автоматического завершения выражений (statement completion), и вы увидите сигнатуры функций и типы точно так же, как и при прямой ссылке на jQuery. Кроме того, вы можете создать единый файл объявлений (например, myRef.d.ts), содержащий ссылки на все библиотеки, которые вы намерены задействовать в своем решении; после этого в любом коде на TypeScript достаточно будет одной ссылки.
Windows 8 и TypeScript
HTML5 является полноправным инструментарием в разработке приложений Windows Store, и разработчики интересуются, можно ли использовать TypeScript с приложениями этого типа. Краткий ответ — да, но для этого потребуется некоторая подготовительная работа. На момент написания этой статьи инструментарий, доступный либо через Visual Studio Installer, либо через другие расширения, не полностью поддерживал шаблоны приложений JavaScript Windows Store в Visual Studio 2012.
В исходном коде на typescript.codeplex.com есть три ключевых файла объявлений: winjs.d.ts, winrt.d.ts и lib.d.ts. Ссылки на эти файлы дадут вам доступ к JavaScript-библиотекам WinJS и WinRT, используемым в этой среде для обращения к камере, системным ресурсам и т. д. Кроме того, вы можете добавить ссылки на jQuery, чтобы получить поддержку IntelliSense и безопасности типов, о которой я рассказывал в этой статье.
На рис. 8 приведен краткий пример, показывающий, как использовать эти библиотеки для доступа к данным геопозиционирования и заполнения класса Location. Затем этот код создает HTML-тег image и добавляет статическую карту из Bing Map API.
Рис. 8. Файлы объявлений для Windows 8
/// <reference path="winjs.d.ts" />
/// <reference path="winrt.d.ts" />
/// <reference path="jquery.d.ts" />
module Data {
class Location {
longitude: any;
latitude: any;
url: string;
retrieved: string;
}
var locator = new Windows.Devices.Geolocation.Geolocator();
locator.getGeopositionAsync().then(function (pos) {
var myLoc = new Location();
myLoc.latitude = pos.coordinate.latitude;
myLoc.longitude = pos.coordinate.longitude;
myLoc.retrieved = Date.now.toString();
myLoc.url = "http://dev.virtualearth.net/REST/v1/Imagery/Map/Road/"
+ myLoc.latitude + "," + myLoc.longitude
+ "15?mapSize=500,500&pp=47.620495,-122.34931;21;AA&pp="
+ myLoc.latitude + "," + myLoc.longitude
+ ";;AB&pp=" + myLoc.latitude + "," + myLoc.longitude
+ ";22&key=BingMapsKey";
var img = document.createElement("img");
img.setAttribute("src", myLoc.url);
img.setAttribute("style", "height:500px;width:500px;");
var p = $("p");
p.append(img);
});
};
Заключение
Функциональность, которую TypeScript добавляет в разработку на JavaScript, невелика, но дает большие преимущества .NET-разработчикам, привыкшим к сходной функциональности в языках, которыми они регулярно пользуются для создания обычных Windows-приложений.
TypeScript — это не решение всех проблем, и никто его таким и не задумывал. Но любому, кто сомневается, стоит ли заниматься программированием на JavaScript, TypeScript станет великолепным подспорьем.