TypeScript: общие впечатления

Думаю, многие из вас знают, что сейчас существует обилие различных языков (или инструментов), которые позволяют писать код компилируемый в 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.
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 41
  • 0
    Вы рассматривали flow? Может быть расскажете про сходства/различия?
    • +3
      Сходства? Есть только различия:
      TypeKit — язык
      Flow — статический анализотор JavaScript
      • +4
        Брр, бес попутал — конечно TypeScript.
    • +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#, а найти что-то среднее.
      • +2
        И ещё: не понял, чем вам не нравятся определения конструктора.
        interface SomeType {
            new (value: string): Instance;
            new (value: number, value2: {}): Instance;
        }
        
        • 0
          Очевидно, это был мой пробел.
          Спасибо, не знал про new в интерфейсе.

          Попробовал использовать то, что вы написали, и получил ошибку.
          Буду очень вам признателен, если вы сможете объяснить в чем ошибка: ссылка на Playground
          • 0
            Этот случай описан здесь: www.typescriptlang.org/Handbook#interfaces-class-types (пункт Difference between static/instance side of class)

            На мой взгляд решение предложено довольно странное.
            • 0
              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.


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

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

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

                Но код не компилируется по абсолютно правильной причине, у вас интерфейс SomeType реализует тип typeof SomeClass (т.е. глобальная джаваскрипт переменная SomeClass), а не инстанс SomeClass, как вы описали.
          • 0
            Вообще, с точки зрения классического ООП, конструктор — это часть реализации, а не интерфейса.
            Думаю, авторами языка этот костыль был внедрен для того, чтобы можно было писать дефиниции на уже существующие библиотеки (в которых вполне может быть полиморфизм в конструкторе, а в TS полиморфизма в таком понимании нет)
            • 0
              Это нужно хотя бы чтобы описать стандартную библиотеку.
          • 0
            var foo: { [key: string]: number; }
            foo = {};
            foo['foobar'] = 10; // OK
            


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

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

              Костыль — это когда приходится писать что-то типа:
              var foo = { bar : 10 };
              foo['__cache__'] = {}; // OK
              
              • +1
                Это не совсем костыль, а скорее «переработка» языка в попытках сделать код типобезопаснее. foo['foobar'] вполне понятно почему разрешен — вы объявили индексатор, и компилятор подчиняется. Вопрос в том, почему нельзя написать foo.foobar — возможно, это сделали так, чтобы не давать разработчику иллюзию типобезопасности. Код вида foo.barbar от foo.baibar на глаз отличается плохо, а в ts привыкаешь, что наличие свойства проверяет компилятор, и ошибка неочевидно. Другое дело — индексатор, в котором достаточно очевидна необходимость удостовериться в идентичности ключей.

                Настоящий костыль — это включаемый в настройках компилятора строковый индексатор у Object, созданный для ленивых людей. :)
              • 0
                Скажите, как писать на нём модули Angular 1.x. Так и не осилил внедрение зависимостей.
                • 0
                  Этого не знаю. Могу разве что дать ссылку на определения типов для фреймворка, может чем-то поможет:

                  github.com/borisyankov/DefinitelyTyped/tree/master/angularjs
                  • 0
                    Вы бы не могли подробнее описать, с чем именно у вас затруднения? Возможно я смогу вам помочь.
                    • 0
                      Делаю фабрику, которая использует $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, но не понял до конца суть этого метода, да и выглядело в той статье всё это ужасно.
                      • 0
                        Разделите внедрение зависимостей + конфигурация и класс фабрики, а зависимости передавайте через конструктор.

                        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;
                        }
                        • 0
                          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) { ... } }

                          (Не могу понять почему не работает)
                          • 0
                            Коментарий выше оказался испорчен потому что не уложился в ограничение в 3 минуты :(
                            Вместо (Не могу понять почему не работает) читать (Извиняюсь за форматирование, почему-то не могу заставить работать тэг )
                  • 0
                    Насколько быстро компилируется крупный проект?
                    • 0
                      TypeScript компилирует каждый файл, отдельно, без зависимостей. Можно выбрать для какой системы модулей компилировать — CommonJS, AMD, ES6. Т.е. зависит напрямую от количества файлов и кода в них.
                      • 0
                        Это всего-лишь трансляция в JS. После неё идёт проверка типов, для которой, по понятным причинам, нужен код всего проекта.
                        • +2
                          Не совсем правда: MSBuild умеет запускать билд всех файлов в проекте одним процессом, а после одного билда не запускать перекомпиляцию, если ts файлы не менялись. У меня проект из 500 ts файлов с 70 000 строк кода компилируется около 3 секунд.
                        • –11
                          Очень медленно.
                          • –2
                            Разве не правду сказал? Ну ок, значит это я слишком быстро думаю, раз успеваю хабр почитать, пока проект компилируется.
                            • 0
                              После того как они оони повысили скорость компилятора в 4-5 раз я лично проблем не испытваю. Не могли бы вы описать более конкретно: сколько у вас файлов, сколько строк кода в них, как долго идёт компиляция, какая версия компилятора, как компилируете (vs/grunt/gulp ?)
                              • +2
                                Ну, у некоторых и IDEA «не тормозит» :-)
                                Порядка сотни относительно небольших файлов, сборка каждого пакета — 2-3 секунды.
                                Компилирую через Compiler API.
                          • +1
                            Полная сборка проекта ~350К строк занимает 10 секунд.
                            Инкрементальная пересборка webpack'oм(https://github.com/s-panferov/awesome-typescript-loader) в зависимости от фундаментальности изменений занимает 1-3 секунды
                          • 0
                            Дебажить TS-код под node.js с бряками и вотчами уже можно?
                            • 0
                              В IDEA, как минимум, да. Единственный нюанс — стектрейсы ничего не знают про сорсмапы.
                              • +6
                                А есть issue в трекере? Я из JetBrains, могу починить.
                                • 0
                                  Не знаю. Речь идёт о стектрейсах, что пишутся в логи. Тут разве что можно при переходе из консоли к файлу понимать, что для него есть сорсмап и открывать исходник. Ещё, к слову, дебаггер в IDEA не очень дружит с node-fibers — после возобновления волокна дебаггер не может получить доступ к содержимому переменных.
                                • 0
                                  А если использовать что-нибудь вроде этого?
                                  • 0
                                    Спасибо большое. То, что надо :-)
                                • 0
                                  Visual Studio Professional 2013 + nodejstools.codeplex.com + typescript на backend и frontend — полёт нормальный. Раньше не всегда срабатывал маппинг и брейкпойнт останавливался в нужном месте, но в файле *.js. С апдейтами NTVS, это проблема встречается все реже.
                                • +1
                                  Кстати, на edx.org 2 июня начинается курс по typescript
                                  • 0
                                    Генерируемый JS очень похож на исходный код на TS и дебажить его довольно просто, это не создаёт никаких проблем, по крайней мере мне.
                                    • +1
                                      Не понял проблему с typeof. Конструктор и экземпляр разные же вещи.
                                      • 0
                                        Так и есть. В примере в статье функция ожидает получить конструктор, а не экземпляр. Поэтому в качестве типа указывается конструкция typeof , которая указывает на потребность получения конструктора класса, а не его экземпляра. Подробнее можно почитать об этом здесь

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.