Альтернативы JavaScript. Часть 1

Надоели «сюрпризы» JavaScript? Не понимаете, как можно жить без классов? Не приемлете идею прототипированного наследования, и вас просто трясет от отсутствия строгой типизации? А с другой стороны, без JavaScript сейчас ведь просто не обойтись… Беда. Но выход есть! Даже несколько!

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

TypeScript – JavaScript от Microsoft

TypeScript – язык, позиционируемый как средство разработки веб-приложений, расширяющее возможности JavaScript. Он был представлен корпорацией Microsoft в 2012 году. Примечательна личность разработчика – Андерса Хейлсберга (Anders Hejlsberg), принимавшего деятельное участие в создании Turbo Pascal, Delphi и C#. В отличие от CoffeeSript, TypeScript является обратносовместимым с JavaScript. Он компилируется в JavaScript, после чего программу на TypeScript можно выполнять в любом современном браузере. Как и CoffeeSсript, его вполне можно использовать совместно с платформой Node.js.

Отличия TypeScript от JavaScript:

  • возможность явного определения типов (статическая типизация);
  • поддержка использования полноценных классов (как и традиционная объектно-ориентированная модель);
  • поддержка подключения модулей.

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

Node.js как TypeScript-компилятор

Ну, теперь за дело. TypeScript действительно нуждается в компиляции, и путей для нее ровно два – использовать Microsoft Visual Studio (от 2012) или соответствующий модуль платформы Node.js. Последним мы и воспользуемся для наших примеров. Сначала установим сам модуль:

npm install -g typescript

Пользоваться им просто. Создаем простой файл с TypeScript-кодом (hello.ts):

function greeter(person: string) {
    return "Hello, " + person;
}
console.log(greeter("TypeScript"));

Теперь компиляция. Набираем в консоли:

$ tsc hello.ts

Результатом будет файл hello.js:

function greeter(person) {
    return "Hello, " + person;
}
console.log(greeter("TypeScript"));

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

Аннотации типов

Что нам может предложить TypeScript? Прежде всего статическую типизацию, мы ее наблюдали в первом же примере:

function greeter(person: string){...}

Тип данных тут указан после имени аргумента. Если мы передадим функции неверный тип данных, при компиляции будет выдано предупреждение:

greeter(1);

Результат:

$ tsc hello.ts
gr.ts(4,13): error TS2082: Supplied parameters do not
match any signature of call target:
Could not apply type 'string' to argument 1 which is of type 'number'.
gr.ts(4,13): error TS2087: Could not select overload f
or 'call' expression.

Можно указать тип возвращаемого значения:

function process(x: number, y: number): number {
    var sum = x + y;
    return sum;
}

А можно вообще ничего не указывать – строгая типизация не является в TypeScript обязательной. Зато функции теперь можно задавать параметры по умолчанию и указывать необязательные аргументы:

function process(x =5, y?: number): number {
    var sum;
    if(!y){
        sum = x;
    } else{
        sum = x + y;
    }
    return sum;
}
console.log(process(2,6)); //8
console.log(process(2)); //2
console.log(process()); //5

Результаты компиляции:

function process(x, y) {
    if (typeof x === "undefined") { x = 5; }
    var sum;
    if (!y) {
        sum = x;
    } else {
        sum = x + y;
    }
    return sum;
}
console.log(process(2, 6)); //8
console.log(process(2)); //2
console.log(process()); //5

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

Классы! Настоящие классы!

Готовы поспорить, ради того все и затевалось. Да, в TypeScript существуют «нормальные» классы и «нормальное» наследование. Пример TypeScript-класса:

class Person {
    name: string;
    surname: string;
    private id: number;
    static title = "Example";
    
    constructor (name: string, surname: string) {
        this.name = name;
        this.surname = surname;
    }

    setID (id) { this.id = id; }
    
    getFullName () { return this.name+" "+this.surname; }
}

console.log(Person.title+":"); // Example:
var User = new Person("Kirill", "Sukhov");
console.log(User.name); // Kirill
console.log(User.getFullName()); // Kirill Sukhov

Тут почти все, о чем мечтали сторонники «традиционного» ООП: поля, методы, конструктор. Имеются и модификаторы доступа – попытка получить значение User.id или установить его значение непосредственно, а не с помощью специальных методов класса Person, потерпит неудачу (модификатор public тоже есть, но его почти всегда можно опустить).

Статические поля и свойства также подчиняются привычным законам – то есть доступны без создания экземпляра класса.

При компиляции этого кода мы получим следующую, не очень сложную JavaScript-конструкцию:

var Person = (function () {
        function Person(name, surname) {
            this.name = name;
            this.surname = surname;
        }
    Person.prototype.setID = function (id) {
        this.id = id;
    };
    Person.prototype.getFullName = function () {
        return this.name + " " + this.surname;
    };
    Person.title = "Example";
    return Person;
})();
console.log(Person.title + ":"); // Example:
var User = new Person("Kirill", "Sukhov");
console.log(User.name);
console.log(User.getFullName());

Теперь наследование. Напишем еще один класс, расширяющий предыдущий:

class Member extends Person {
    band: string;
    constructor(name: string, surname: string, band: string){
        super(name, surname);
        this.band = band;
    }

    getBand () { return this.band; }
}
var User = new Member("John", "Lennon", "The Beatles");
console.log(User.getFullName()); // John Lennon
console.log(User.getBand()); // The Beatles

Мы добавили немного – название группы. В конструкторе мы методом super() вызываем родительский конструктор.

Можно переопределить родительский метод:

class Member extends Person {
    band: string;
    constructor(name: string, surname: string, band: string){
        super(name, surname);
        this.band = band;
    }
    
    getBand () { return this.band; }

    getFullName() {
        return super.getFullName()+" From "+this.band;
    }
}
var User = new Member("John", "Lennon", "The Beatles");
console.log(User.getFullName()); // John Lennon from The Beatles

Интерфейсы

Да, кроме классов, в TypeScript существуют и эти полезные языковые конструкции. Ниже пример простого интерфейса и использующей его функции:

interface Band {
    name: string;
    state?: string;
    members: any;
}

function ShowBand(band: Band) {
    console.log(band.name);
    if(band.state){
        console.log(band.state);
    }
    band.members.forEach( function(member){
        console.log(member);
    });
}

var obj = { name: "Focus",
    state: "nl",
    members: ["Thijs", "Jan", "Martin", "Hans"]
}

ShowBand(obj);

Тут интерфейс контролирует параметры объекта, передаваемого функции как аргумент. Знак вопроса после параметра id указывает на его необязательность. При компиляции кода в JavaScript интерфейс исчезает, он свое дело сделал:

function ShowBand(band) {
    console.log(band.name);
    if (band.state) {
        console.log(band.state);
    }
    band.members.forEach(function (member) {
        console.log(member);
    });
}

var obj = {
    name: "Focus",
    state: "nl",
    members: ["Thijs", "Jan", "Martin", "Hans"]
};

ShowBand(obj);

Модули

Модули в TypeScript организованы по стандартам CommonJS и EcmaScript6. Их задача – инкапсуляция бизнес-логики в отдельной конструкции со своим пространством имен… Хотя, что объяснять, что такое модули в конце статьи про Node.js? Лучше покажу на практике, как они реализованы в TypeScript.

module Say {
    export function Hello(text: string) {
        return "Hello " + text;
    }
}
console.log(Say.Hello("Module"));

Ничего не напоминает? Хотя что, собственно, напоминать, это не сходство, это тот же самый механизм, который мы используем, например, в Node.js. При компиляции получаем следующее:

var Say;

(function (Say) {
    function Hello(text) {
        return "Hello " + text;
    }
    Say.Hello = Hello;
})(Say || (Say = {}));

console.log(Say.Hello("Module"));

Что еще?

Кроме разобранных нами языковых конструкций, TypeScript еще много чем может порадовать разработчика. Например, реализацией примесей (mixins), array-синтаксисом из EcmaScript 6, типом данных Generic. Очень динамично развивающаяся технология, поддерживаемая технологическим гигантом – даже если вы сторонник чистого JavaScript, не стоит обделять вниманием этот проект.

В следующей статье расскажем вам об альтернативе JavaScript от Google — языке программирования Dart.

Материал взят из выпуска №146-147 журнала «Системный Администратор»