Думаю, многие из вас знают, что сейчас существует обилие различных языков (или инструментов), которые позволяют писать код компилируемый в JavaScript. Это CoffeeScript, Dart, GorillaScript и другие (довольно большой список можно найти здесь). Недавно я решил познакомиться с одним из представителей этого списка — языком программирования под названием TypeScript.

Я расскажу о личном опыте — почему решил попробовать именно его, как начать работать с ним, и какие выводы сделал, поработав с ним несколько дней.

Если вам это интересно — пожалуйста, заходите под кат.

Небольшая выжимка в качестве описания этого языка


  • Язык со строгой типизацией
  • Код написанный на TypeScript компилируется в JavaScript
  • Язык обратно совместим с JavaScript — если вы скормите компилятору чистый JavaScript, компилятор выплюнет вам ваш же JS, и не скажет что это ошибка. Можно писать смешанный код (например, модули/методы используя синтаксис TypeScript, а реализацию методов без типизации на чистом JS) — это тоже будет валидно.
  • Язык разработан компанией Microsoft.


Какие возможности открывает язык


  • Система для работы с модулями/классами — можно создать интерфейсы, модули, классы;
  • Можно наследовать интерфейсы (в том числе множественное наследование), классы;
  • Можно описывать собственные типы данных;
  • Можно создавать универсальные-интерфейсы (generic interfaces);
  • Можно описать тип переменной (или свойств объекта), или описать каким интерфейсом должен обладать объект на который ссылается переменная;
  • Можно описать сигнатуру метода.


Конечно, это далеко не все — это лишь основные моменты, которые я выделил.

Основные ссылки




Почему стоит попробовать TypeScript?


Размышляя о том, почему имеет смысл попробовать поработать на TypeScript, я выделил несколько основных пунктов:

  • Возможность жестко описать каждый элемент приложения — это, вероятно, исключает возможность неверной реализации или некорректного вызова методов; Заставляет разработчиков продумать логику (например, в методах) до самой реализации; Не дает возможность изменить один кусок приложения, сломав другой;
  • Возможность описать область видимости свойств класса — еще один барьер, который ограничивает разработчика от совершения ошибок;
  • Из-за жесткой архитектуры, возможно, необходимо писать меньше тестов — все параметры методов жестко описаны, и если код скомпилировался, тогда, вероятно, каждый вызов является валидным, и не требует дополнительной проверки;
  • Можно настроить проект таким образом, что любой некомпилируемый код (фактически, код с синтаксической ошибкой) нельзя будет закоммитить. Случаи в которых разработчик позволяет себе закоммитить сломанный код в обход проверок перед коммитом не рассматриваем;
  • Многие конструкции в TypeScript имеют жесткий формат, поэтому многие из ошибок форматирования кода (в соответствии каким-то принятым в коллективе нормам) исключены. Я думаю что это плюс, потому что процесс ревью кода часто выявляет ошибки форматирования, на которые тратится ценное время. Если это время можно потратить на другие более полезные активности — это уже плюс.


С другой стороны, есть ряд минусов:

  • Чтобы использовать какой-то внешний инструмент (читай «библиотеку» или «фреймворк»), тогда сигнатуру каждого из методов каждого модуля этого инструмента необходимо описать, чтобы компилятор не выбрасывал ошибки (он просто не будет знать про ваш инструмент). Если это популярный инструмент, тогда, вероятнее всего, описание интерфейсов можно найти в этом репозитории. Если нет — придется описывать самому;
  • Вероятно, самый большой минус (возможно, временный) — порог вхождения и количество специалистов на рынке. Сейчас почти нет специалистов которые знают этот язык. Это плохо, потому что любой солидный проект со временем переходит на этап поддержки, и в случае потери специалистов, найти им замену будет сложнее. Возможно, я ошибаюсь, но я пришел к этому выводу опираясь на статистику Github (ссылка) — создано лишь ~6k репозиториев с TypeScript кодом против ~1.4kk на JavaScript;
  • На разработку тратится больше времени, в сравнении с JavaScript. Это вызвано тем, что помимо реализации класса необходимо описать все задействованные интерфейсы, сигнатуры методов.


Начинаем работать


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

Сборка


Прежде всего, было необходимо написать сборщик тестового проекта. Есть несколько NPM пакетов для gulp которые позволяют компилировать TypeScript код в JavaScript. Без знания что выбрать я начал пробовать все пакеты в том порядке, в каком мне их выдал Google. Оказалось, что не все пакеты используют последнюю версию компилятора (последняя версия была 1.5.0), и из-за этого код который компилировался на сайте TypeScript (ссылка) не компилировался плагином для gulp. Методом проб и ошибок я остановился на пакете gulp-tsc, который поддерживает все версии компилятора и работает «на ура».

Компиляция


Каждый интерфейс, сигнатура каждого экспортированного метода: все это должно быть известно компилятору, иначе он откажется компилировать код. Я работал с AMD модулями (об этом чуть позже), и при импорте одних модулей в другие возникала проблема — компилятор совершенно ничего не знал об существовании других модулей.

Для этих целей существуют .d.ts файлы — файлы, в которых нужно определить что конкретно экспортирует тот или иной модуль, какие глобальные переменные и функции определены.

На первый взгляд — все просто. На деле здесь обнаружился подводный камень (смотрим код ниже).

Создадим файл foo.ts в котором определим модуль foo:

/// <amd-module name="foo" />

export = {
	bar: () => 'baz'
}


Создадим файл bar.ts в котором определим модуль bar, который импортирует модуль foo:

/// <amd-module name="bar" />

import foo = require('foo'); // Ошибка: "Cannot find external module 'foo'."

export = {
	foo: foo
}


Мы получили ошибку «Не удается найти внешний модуль foo.». Почему так происходит? Так происходит потому что этот модуль мы нигде не определили и компилятор про него не знает.

Создадим файл foo.d.ts, в котором расскажем компилятору, что есть такой модуль foo, и он экспортирует один метод bar:

declare module foo {
	export function bar(): string
}


Мы добавили определение модуля и теперь все похоже на правду, все должно заработать, не правда ли? Неправда, потому что ровным счетом ничего от этого не изменилось — компилятор по-прежнему не может найти модуль foo. Вопрос — почему?

Решение оказалось неожиданным — название модуля было определено не в кавычках.

Работающий код файла foo.d.ts:

declare module 'foo' {
	export function bar(): string
}


Едем дальше...

AMD


Конечно, если TypeScript дает возможность создавать AMD-модули и можно красиво импортировать зависимости, почему бы этим не воспользоваться хотя бы для теста?

Я попробовал — в TypeScript по-определению нельзя генерировать модули с именами. TypeScript дает возможность генерировать модули без имен, не более. Мне показалось, что это было бы странно, и оказалось, что это можно обойти.

Пример модуля, который будет скомпилирован в модуль с именем:

/// <amd-module name="foo" />

export = {
	bar: () => 'baz'
}


Что еще интересного?


Большая часть времени, проведенного за кодированием на TypeScript, не доставила проблем, но довольно часто возникают тонкие моменты, решение которых довольно трудно найти. Одна из таких проблем, которую я не смог решить на текущий момент — как описать объект, свойства которого будут динамически определяться (названия свойств неизвестны), но каждый из них должен содержать объект, который имеет строго определенный интерфейс?

Пример неработающего кода:

interface IBar {
	baz: string
}

var foo: {
	[property: string]: IBar
}

foo = {};

foo.foobar = {
	baz: 'Hi there!'
}

Пример работающего кода:

interface IBar {
	baz: string
}

var foo: {
	[property: string]: IBar
}

foo = {
	foobar: {
		baz: 'Hi there!'
	}	
};

Это, конечно, не все проблемы, которые возникли, но каждая из них носит именно такой характер, как в примере выше.

Небольшой список других проблем:

  • Нельзя красиво определить сигнатуру конструктора;
  • Чтобы сказать TypeScript что мы ожидаем в методе получить конструктор в качестве параметра, а не экземпляр класса, нужно написать function foo (bar: typeof Baz) {… new Baz(...)… } а не просто function foo (bar: Baz) {… new Baz(...)… }.

Небольшой итог


Для себя я отметил TypeScript как отличный инструмент, который позволит исключить некоторые риски (в основном, исключение возможности ошибки, и все проблемы из этого вытекающие), позволяет жестко описать взаимодействие между элементами приложения, предлагает базовые инструменты для работы с модулями/классами и компилируется не в бороду из кода, а в достаточно читаемый код.

P.S Очень вероятно, что некоторые из проблем возникли лишь потому, что я еще не умею «готовить» TypeScript и поэтому некоторая информация может быть не объективно точной, но даже в этом случае я надеюсь, что эта информация окажется кому-нибудь полезной.

P.P.S Буду рад комментариям от знатоков TypeScript.

Комментарии (40)


  1. mkuzmin
    27.05.2015 20:36

    Вы рассматривали flow? Может быть расскажете про сходства/различия?


    1. Ashot
      27.05.2015 23:27
      +3

      Сходства? Есть только различия:
      TypeKit — язык
      Flow — статический анализотор JavaScript


      1. Ashot
        27.05.2015 23:43
        +4

        Брр, бес попутал — конечно TypeScript.


  1. a553
    27.05.2015 20:50
    +12

    Использую пару лет в продакшене, пишу на TS с самого зачатия.

    Чтобы использовать какой-то внешний инструмент (читай «библиотеку» или «фреймворк»), тогда сигнатуру каждого из методов каждого модуля этого инструмента необходимо описать
    Не обязательно, можно «заткнуть» какую-то библиотеку через any. Например, допустим нет определений для jQuery, но её можно использовать просто описав так: var $: any;

    а разработку тратится больше времени, в сравнении с JavaScript. Это вызвано тем, что помимо реализации класса необходимо описать все задействованные интерфейсы, сигнатуры методов.
    Это достаточно распространенное заблуждение. На самом деле, типы можно определять «на ходу», т.е. inline. Например: f(arg: { p: number; }): void; — это функция, которая принимает такой объект, у которого есть свойство p типа number. Или так: f() { return { p: 10; }; } — возвращает автоматически выведенный тип { p: number; }.

    Код такого вида невалиден:
    var foo: { [key: string]: number; }
    foo = {};
    foo.foobar = 10; // error
    
    Потому что вы не определили свойство foobar. Но можно так:
    var foo: { [key: string]: number; }
    foo = {};
    foo['foobar'] = 10; // OK
    


    Реальная проблема TS: достаточно сложно отучиться писать его как JavaScript, не писать на нём как на C#, а найти что-то среднее.


    1. a553
      27.05.2015 20:55
      +2

      И ещё: не понял, чем вам не нравятся определения конструктора.

      interface SomeType {
          new (value: string): Instance;
          new (value: number, value2: {}): Instance;
      }
      


      1. RomanYakimchuk Автор
        28.05.2015 07:56

        Очевидно, это был мой пробел.
        Спасибо, не знал про new в интерфейсе.

        Попробовал использовать то, что вы написали, и получил ошибку.
        Буду очень вам признателен, если вы сможете объяснить в чем ошибка: ссылка на Playground


        1. some_x
          28.05.2015 08:49

          Этот случай описан здесь: www.typescriptlang.org/Handbook#interfaces-class-types (пункт Difference between static/instance side of class)

          На мой взгляд решение предложено довольно странное.


          1. RomanYakimchuk Автор
            28.05.2015 09:02

            some_x, спасибо за ссылку.

            Я думаю что написанному на странице по ссылке есть объяснение, но для меня оно пока не очевидно.

            Исходя из прочитанного, я склонен думать что решение предложенное по ссылке является костылем:

            This is because when a class implements an interface, only the instance side of the class is checked. Since the constructor sits in the static side, it is not included in this check.


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

            Лично у меня создалось ощущение что авторы языка столкнулись с каким-то блокером при реализации описания сигнатуры конструктора, и чтобы не решать проблему просто решили ее «обойти».

            Возможно, я не прав, но впечатление создалось именно такое.


            1. a553
              28.05.2015 11:55

              Да, такая проблема есть — язык пока недостаточно выразительный. Из-за этого же нельзя нормально сделать расширемые встроенные классы: github.com/Microsoft/TypeScript/issues/1168

              Но код не компилируется по абсолютно правильной причине, у вас интерфейс SomeType реализует тип typeof SomeClass (т.е. глобальная джаваскрипт переменная SomeClass), а не инстанс SomeClass, как вы описали.


      1. koroandr
        29.05.2015 13:46

        Вообще, с точки зрения классического ООП, конструктор — это часть реализации, а не интерфейса.
        Думаю, авторами языка этот костыль был внедрен для того, чтобы можно было писать дефиниции на уже существующие библиотеки (в которых вполне может быть полиморфизм в конструкторе, а в TS полиморфизма в таком понимании нет)


        1. a553
          29.05.2015 16:43

          Это нужно хотя бы чтобы описать стандартную библиотеку.


    1. RomanYakimchuk Автор
      28.05.2015 07:45

      var foo: { [key: string]: number; }
      foo = {};
      foo['foobar'] = 10; // OK
      


      Возможно, я ошибаюсь, но я склонен считать что это костыль.
      Фактически, сделав так — мы обманули компилятор чтобы решить свою собственную задачу.

      a553,
      Это нормальная практика?
      Много бывает ли кейсов, когда приходится прибегать к решениям такого типа?


      1. vintage
        28.05.2015 10:28

        Ошибаетесь. В интерфейсе мы объявили foo как словарь. Так что логично, что обращаться к нему надо как ко словарю, а не как к объекту. То, что в JS все объекты — словари и можно использовать обе формы записи — особенность объектной модели JS.

        Костыль — это когда приходится писать что-то типа:

        var foo = { bar : 10 };
        foo['__cache__'] = {}; // OK
        


      1. a553
        28.05.2015 11:51
        +1

        Это не совсем костыль, а скорее «переработка» языка в попытках сделать код типобезопаснее. foo['foobar'] вполне понятно почему разрешен — вы объявили индексатор, и компилятор подчиняется. Вопрос в том, почему нельзя написать foo.foobar — возможно, это сделали так, чтобы не давать разработчику иллюзию типобезопасности. Код вида foo.barbar от foo.baibar на глаз отличается плохо, а в ts привыкаешь, что наличие свойства проверяет компилятор, и ошибка неочевидно. Другое дело — индексатор, в котором достаточно очевидна необходимость удостовериться в идентичности ключей.

        Настоящий костыль — это включаемый в настройках компилятора строковый индексатор у Object, созданный для ленивых людей. :)


    1. dunmaksim
      28.05.2015 17:03

      Скажите, как писать на нём модули Angular 1.x. Так и не осилил внедрение зависимостей.


      1. a553
        28.05.2015 17:22

        Этого не знаю. Могу разве что дать ссылку на определения типов для фреймворка, может чем-то поможет:

        github.com/borisyankov/DefinitelyTyped/tree/master/angularjs


      1. some_x
        29.05.2015 07:59

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


        1. dunmaksim
          29.05.2015 09:00

          Делаю фабрику, которая использует $resource и $sce. На JS пишу так:

          (function (A){
              "use strict";
              A.module('app').factory('FactoryName', [ '$resource', '$sce', function($resource, $sce){
                  var r = $resource('/api/url/:id/', {
                      id: '@id'
                  }, {
                      update: {
                          method: 'PATCH'
                      }
                  });
          
                  function Factory(){
                      // Код инициализации
                  }
          
                  // Расширение прототипа свойствами и методами
                  Factory.prototype.someMethod = function(){
                      return $sce.trustAsHtml(this.someProperty);
                  };
          
                  return Factory;
              }]);
          }(this.angular));


          Как написать то же самое на TS? Видел где-то, как внедряются зависимости через $inject, но не понял до конца суть этого метода, да и выглядело в той статье всё это ужасно.


          1. sferrka
            29.05.2015 14:13

            Разделите внедрение зависимостей + конфигурация и класс фабрики, а зависимости передавайте через конструктор.

            angular.module('app').factory('FactoryName', ['$resource', '$sce', function ($resource, $sce) {
                var apiObjectResource = $resource('/api/url/:id/', {
                    id: '@id'
                }, {
                    update: {
                        method: 'PATCH'
                    }
                });
                return new Factory($sce, apiObjectResource);
            }]); 
            
            class Factory {
                $sce: ng.ISCEService;
                apiObjectResource: angular.resource.IResource<APIObject>;
                someProperty;
                constructor($sce, apiObjectResource) {
                    this.$sce = $sce;
                    this.apiObjectResource = apiObjectResource;
                }
                someMethod() {
                    return this.$sce.trustAsHtml(this.someProperty);
                };
            }
            class APIObject {
                id: number;
            }


          1. some_x
            29.05.2015 16:29

            sferrka верно написала. Через $inject удобно внедрять зависимость например в класс контроллера

            class SwiperController { public static thumbnailsMaxWidth = 55; public static thumbnailsMaxHeight = 33; public static $inject = ["$element", "$scope"]; constructor(private swiperRootElement: ng.IAugmentedJQuery, private _scope: ng.IScope) { ... } }

            (Не могу понять почему не работает)


            1. some_x
              29.05.2015 16:37

              Коментарий выше оказался испорчен потому что не уложился в ограничение в 3 минуты :(
              Вместо (Не могу понять почему не работает) читать (Извиняюсь за форматирование, почему-то не могу заставить работать тэг )


  1. Claud
    27.05.2015 23:06

    Насколько быстро компилируется крупный проект?


    1. sferrka
      28.05.2015 00:15

      TypeScript компилирует каждый файл, отдельно, без зависимостей. Можно выбрать для какой системы модулей компилировать — CommonJS, AMD, ES6. Т.е. зависит напрямую от количества файлов и кода в них.


      1. vintage
        28.05.2015 02:48

        Это всего-лишь трансляция в JS. После неё идёт проверка типов, для которой, по понятным причинам, нужен код всего проекта.


      1. a553
        28.05.2015 04:44
        +2

        Не совсем правда: MSBuild умеет запускать билд всех файлов в проекте одним процессом, а после одного билда не запускать перекомпиляцию, если ts файлы не менялись. У меня проект из 500 ts файлов с 70 000 строк кода компилируется около 3 секунд.


    1. vintage
      28.05.2015 02:46
      -11

      Очень медленно.


      1. vintage
        28.05.2015 10:33
        -2

        Разве не правду сказал? Ну ок, значит это я слишком быстро думаю, раз успеваю хабр почитать, пока проект компилируется.


        1. some_x
          28.05.2015 13:15

          После того как они оони повысили скорость компилятора в 4-5 раз я лично проблем не испытваю. Не могли бы вы описать более конкретно: сколько у вас файлов, сколько строк кода в них, как долго идёт компиляция, какая версия компилятора, как компилируете (vs/grunt/gulp ?)


          1. vintage
            28.05.2015 14:48
            +2

            Ну, у некоторых и IDEA «не тормозит» :-)
            Порядка сотни относительно небольших файлов, сборка каждого пакета — 2-3 секунды.
            Компилирую через Compiler API.


    1. irezvov
      28.05.2015 10:34
      +1

      Полная сборка проекта ~350К строк занимает 10 секунд.
      Инкрементальная пересборка webpack'oм(https://github.com/s-panferov/awesome-typescript-loader) в зависимости от фундаментальности изменений занимает 1-3 секунды


  1. musuk
    28.05.2015 01:01

    Дебажить TS-код под node.js с бряками и вотчами уже можно?


    1. vintage
      28.05.2015 02:49

      В IDEA, как минимум, да. Единственный нюанс — стектрейсы ничего не знают про сорсмапы.


      1. Develar
        28.05.2015 09:51
        +6

        А есть issue в трекере? Я из JetBrains, могу починить.


        1. vintage
          28.05.2015 10:39

          Не знаю. Речь идёт о стектрейсах, что пишутся в логи. Тут разве что можно при переходе из консоли к файлу понимать, что для него есть сорсмап и открывать исходник. Ещё, к слову, дебаггер в IDEA не очень дружит с node-fibers — после возобновления волокна дебаггер не может получить доступ к содержимому переменных.


      1. MuLLtiQ
        28.05.2015 17:28

        А если использовать что-нибудь вроде этого?


        1. vintage
          28.05.2015 22:21

          Спасибо большое. То, что надо :-)


    1. 1vanu4
      28.05.2015 10:53

      Visual Studio Professional 2013 + nodejstools.codeplex.com + typescript на backend и frontend — полёт нормальный. Раньше не всегда срабатывал маппинг и брейкпойнт останавливался в нужном месте, но в файле *.js. С апдейтами NTVS, это проблема встречается все реже.


  1. AtomKrieg
    28.05.2015 07:10
    +1

    Кстати, на edx.org 2 июня начинается курс по typescript


  1. Vestild
    28.05.2015 11:06

    Генерируемый JS очень похож на исходный код на TS и дебажить его довольно просто, это не создаёт никаких проблем, по крайней мере мне.


  1. gro
    28.05.2015 15:53
    +1

    Не понял проблему с typeof. Конструктор и экземпляр разные же вещи.