вторник, 28 октября 2014 г.

Введение в TypeScript



Альтернатива JavaScript от Microsoft добавляет модульность, дженерики и информацию о типах, сохраняя совместимость на уровне исходного кода JS.

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

TypeScript начинается и заканчиватся с JavaScript

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

function changeDirection(s) {
    if (Math.random() > 0.5) {
        s.goLet = true; // -- Silent error  
    }
    else {
        s.goRight = true;
    }

    return s;
}
var s = { goLeft: false, goRight: false };
s = changeDirection(s);
 
TypeScript позволяет добавлять в JavaScript информацию о типах и после этого выполнять этот обновленный код в компиляторе. Эти типы абсолютно опциональны, пользователю нет необходимости  обязательно указывать всю информацию о типах в приложении, только для того что бы он мог воспользоваться этой возможностью в некотором конкретном случае.
Для того что бы упростить идентификацию ошибки описанной выше, мы может добавить аннотацию, которая описывает ожидаемую структуру объекта changeDirection.

function changeDirection(s: { goLeft: boolean; goRight: boolean }) {
    if (Math.random() > 0.5) {
        s.goLet = true; // -- The property 'goLet' does not exist on value
                        // of type '{ goLeft: boolean; goRight: boolean; }'.
    }
    else {
        s.goRight = true;
    }

    return s;
}

var s = { goLeft: false, goRight: false };
s = changeDirection(s);


Так же мы можем улучшить код и добавить интерфейсы. Интерфейсы в TypeScript работают иначе чем в других языках, например таких как C# или Java, здесь они как бы неявно описывают структуру объекта, а не требуют от пользователя реализовывать этот интерфейс.

interface Direction {
    goLeft: boolean;
    goRight: boolean;
}

function changeDirection(s: Direction) {
    if (Math.random() > 0.5) {
        s.goLeft = true;
    }
    else {
        s.goRight = true;
    }

    return s;
}

var s = { goLeft: false, goRight: false };
s = changeDirection(s);


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

TypeScript поддерживает дженерики

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

function filterSmiths(arr: { lastName: string }[]): { lastName: string }[] {
    var result = [];
    for (var i in arr) {
        if (arr[i].lastName == "Smith") {
            result.push(arr[i]);
        }
    }
    return result;
}
 
Когда мы вызываем функцию filterSmiths, мы намереваемся обновить поля объекта и вернуть тотже тип, который бул передан функции. Если мы передаем в функцию обект у которого больше чем одно поле .lastName, то мы также не хотим терять эти поля и в возвращаемом типе.

Var result = filterSmiths([
    { firstName: "Bob", lastName: "Smith" },
    { firstName: "Sam", lastName: "Jones" }
]);  //lost .firstName in the result type


С дженериками мы может поддерживать информацию о типе. Если мы изначально переключимся на использование джнерик типа, то увидим следующую ошибку:

function filterSmiths(arr: T[]): T[] {
    var result = [];
    for (var i in arr) {
        if (arr[i].lastName == "Smith") {
            result.push(arr[i]);  //error: The property 'lastName' does
                                  //not exist on value of type 'T'.
        }
    }
    return result;
}


Компилятор не обладает информацией о типе T и не знает есть ли там поле.lastName . Для того что бы исправить ситуацию, мы должны создать ограничение – минимальное описание объекта типа T который будет использоваться в функции.

interface HasLastName {
    lastName: string;
}

function filterSmiths(arr: T[]): T[] {
    var result = [];
    for (var i in arr) {
        if (arr[i].lastName == "Smith") {
            result.push(arr[i]);
        }
    }
    return result;
}


TypeScript поддерживает и прочее

Ключевым преимуществом TypeScript является его возможность работать с уже существующим кодом JavaScript ваших проектов и сторонних библиотек используемых в коде. Имеется ввиду, что вам не нужно переписывать горы кода для того что бы получить поддержку TypeScript. Вместо этого, TypeScript позволяет описывать объекты, видимые для вашего приложения, но которые возможно были загружены откудато извне. Это касается библиотек JavaScript, например JavaScript, Backbone и AngularJS, которые имеют ключевое значение для вашего приложения.
Давайте возьмем к примеру JQuery. Эта библиотека определяет символ $ и делает его доступным в рантайме, позволяя разработчикам получать доступ к дополнительным возможностям библиотеки. Мы можем полностью описать для компилятора API JQuery, но в качестве первого шага, все что нужно сделать это сказать компилятору что этот символ будет доступен в рантайме.

declare var $: any;

Это выражение говорит компилятору что символ $ создан не нашим приложением, а каким то внешним скриптом или библотекой, который была выполнена до выполнения текущего скрипта. Также оно говорит что тип этой переменной any. Обладая данной информацией, компилятор позволит вам обращатся к любым полям и функциям данной переменной безо всяких ограничений. Это выражение позволяет настроить работу TypeScript  с JQuery максимально быстро. Но это не позволит компилятору отслеживать ошибки и показывать авто-подсказки, поскольку компилятор не обладает детальной информацией о символе $.
Для того что бы это исправить нам нужно иметь API библиотеки JQuery задокументированной для компилятора. К счастью, волонтеры уже проделали огромную работу и подготовили документацию по API многих библиотек, включая jQuery. Ее можно найти в репозитории на GitHub. Для того что бы использовать эти файлы документации, которые называются файлами .d.ts , необходимо подключить эти файлы в проекте. Ниже представлен пример .d.ts файла для jQuery:

// The jQuery instance members

interface JQuery {
    // AJAX
    ajaxComplete(handler: any): JQuery;
    ajaxError(handler: (evt: any, xhr: any, opts: any) => any): JQuery;
    ajaxSend(handler: (evt: any, xhr: any, opts: any) => any): JQuery;
    ajaxStart(handler: () => any): JQuery;
    ajaxStop(handler: () => any): JQuery;
    // ...
}

declare var $: JQueryStatic;

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

Модулярность в TypeScript

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

    export interface Direction {
        goLeft: boolean;
        goRight: boolean;
    }

    export function changeDirection(s: Direction) {
        if (Math.random() > 0.5) {
            s.goLeft = true;
        }
        else {
            s.goRight = true;
        }

        return s;
    }
}

var s = { goLeft: false, goRight: false };
s = RoadMap.changeDirection(s);
 
Второй тип модулей – это внешние модули. Они позволяют отдельным файлам мести себя как модули. Преимуществом внешних модулей является то что они могут быть загружены с помощью одного из популярных загрузчиков модулей JavaScript. Эти загрузчики модулей избавляют от необходимости вручную подгружать требуемые файлы JavaScript. Вместо этого загрузчики модулей сперва загружают зависимости модулей и выполняют автоматическую обработку модулей перед загрузкой самих модулей. В конеченом счете получается набор модулей с понятным определением их зависимостей и разедением между модлуями на уровне компилятора.
Ниже представлен предыдущий пример, которые преобразован в два внешних модуля:  

//RoadMap.ts
export interface Direction {
    goLeft: boolean;
    goRight: boolean;
}

export function changeDirection(s: Direction) {
    if (Math.random() > 0.5) {
        s.goLeft = true;
    }
    else {
        s.goRight = true;
    }

    return s;
}

//Main.ts
import RoadMap = require("RoadMap");
var s = { goLeft: false, goRight: false };
s = RoadMap.changeDirection(s);

Обратите внимание на то что сейчас у нас есть два отдельных фалйа, каждый из которых импортируется или экспортируется непосредственно из файла. Файл RoadMap.ts является отдельным внешним модулем, который описывается именем файла. В файле Main.ts мы подгружаем RoadMap.ts используя директиву import. Это демонстрирует как мы описываем зависимости между двумя модулями. После импорта мы можем осуществялть взаимодействие в с модулем как и прежде.
Для компиляции внешних модулей, мы также должны указать компилятору тип  загрузчика модулей, который мы будем использовать. В настоящее время компилятор поддерживает два стиля: AMD/RequireJS и Node/CommonJS. Для компиляции AMD, мы передаем AMD как тип модуля компилятору:

> tscRoadMap.tsMain.ts--moduleAMD
 
В результате получим JavaScript файлы, которые будут адаптированы для загрузчиков модулей AMD-типа, таких как RequireJS. Например в результате копиляции Main.ts получим следующий код:

define(["require", "exports", "RoadMap"],
    function (require, exports, RoadMap) {
        var s = { goLeft: false, goRight: false };
        s = RoadMap.changeDirection(s);
    });

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

Выведение типов в TypeScript

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

var x = 3;  // x has type number

Следующий пример выводит тип в противоположном направлении, путем вывода типа слева направо. Если у переменной при объявлении указан тип, мы можем вывести информацию о типе для выражения инициализации. В данном примере параметр x в функции которая находится справа имеет выведенный тип, который базируется на типе выражения.

var f: (x: number) => string = function (x) { return x.toString(); }

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

function f(g: (x: number) => string) { return g(3); }
f(function (x) { return x.toString(); })
 
Мы можем переписать предыдущие примеры используя лямбда-выражения, для лучшего понимания как контекстные типы помогают содержать код в читабельном виде и уменьшать количество всевозможного мусора в коде.

function f(g: (x: number) => string) { return g(3); }
f(x => x.toString());


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


Выводы

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