javascript

Первая демонстрация TypeScript

  • суббота, 21 октября 2017 г. в 03:13:31
https://habrahabr.ru/company/ruvds/blog/340556/
  • Разработка веб-сайтов
  • JavaScript
  • Блог компании RUVDS.com


Совсем недавно состоялась пятая годовщина TypeScript, и у меня появилась возможность увидеться с Андерсом и командой разработчиков на еженедельном техническом совещании. Мне хотелось поздравить их с важной вехой в жизни TypeScript, сказать им о том, как много они сделали за те четыре года, которые прошли с даты моего ухода из проекта.



Кроме того, я разыскал некоторые старые демки, сделанные на TypeScript в самом начале работы над проектом (тогда он назывался Strada), для того, чтобы посмотреть их вместе с теми, кто работает над TypeScript сейчас.

Сегодня я хочу рассказать о том, с чего начинался TypeScript.

Начало


Осенью 2010-го мы со Стивом Лукко приступили к работе над новым проектом. Его целью была помощь командам разработчиков приложений в управлении большими объёмами JavaScript-кода. Стив поговорил с несколькими техническими директорами в главном офисе. Они рассказали о стремительном переходе их отделов с C++ / C# на JavaScript, о том, что теперь серьёзные силы вкладываются в развитие веб-приложений и соответствующих сценариев взаимодействия с программами. Командам разработчиков не хватало качественных инструментов, вроде Visual Studio, и тех удобств, которые давали системы типов C++ и C#. Им хотелось бы знать, что мы можем сделать для того, чтобы облегчить переход на JavaScript. Тогда же мы обратили внимание на мощный IT-тренд, идущий в похожем направлении.

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

К тому времени уже существовало немало вариантов решения вышеозначенной проблемы, но ни один из них нас не устраивал, не соответствовал ожиданиям достаточно широкой рыночной ниши. Внутри Microsoft некоторые большие команды использовали Script#. Это позволяло им использовать C# вместо JavaScript. Однако, в результате они оказывались слишком далеко от той среды, для которой они, на самом деле, программировали. Это вызывало определённые неудобства. Был ещё Google Closure Compiler, который предлагал богатую систему типов, внедряемых в комментарии внутри JS-кода, что позволяло управлять некоторыми продвинутыми процессами минификации (и, заодно, отслеживать ошибки, связанные с типами и сообщать о них). И, наконец, тогда был период расцвета CoffeeScript. CoffeeScript стал первым широко используемым языком, транспилируемым в JavaScript, который проложил путь к транспиляторам в JS-разработке. Между прочим, в самом начале, я часто рассказывал об особенностях TypeScript, используя следующую аналогию: «CoffeeScript: TypeScript :: Ruby: C#/Java/C++», часто добавляя: «существует в 50 раз больше C#/Java/C++ разработчиков, чем тех, кто пишет на Ruby :)».

Мы быстро поняли, что нам хотелось бы создать нечто, находящееся на пересечении трёх вышеупомянутых технологий, что-то такое, что могло бы стать во всех отношениях лучше того, что уже было. В нашем представлении это должен был быть язык, семантика и синтаксис которого максимально близки к JavaScript. Такими свойствами, соответственно, обладали CoffeeScript и Closure Compiler. Далее, нам хотелось, чтобы этот язык поддерживал проверку типов и удобные инструментальные средства, то есть, чтобы он взял всё лучшее от Script#.

Мы начали работу над TypeScript как над второстепенным проектом осенью-зимой 2010-го. Для того, чтобы не расслабляться, чтобы результаты нашей работы можно было показать окружающим, что мотивирует, мы запланировали внутреннюю презентацию с демонстрацией того, что мы сделали. Это должна была быть мини-конференция, на которую были приглашены сотрудники, занимающиеся языками программирования, из исследовательской группы и группы разработки ПО в Microsoft. Однако, за месяц до мероприятия, Стив (единственный инженер в команде на то время) повредил запястье, что не дало ему нормально программировать. В итоге, не желая уклоняться от запланированного выступления, я решил собрать всё, что позволило бы нам продемонстрировать то, к чему мы стремимся, даже не имея реально работающего компилятора. (Скоро Стив сделал первый настоящий компилятор TypeScript, который позволил нашим первым внутренним пользователям — команде, которая создавала то, что позже превратилось в VS Code — начать использование TypeScript для решения реальных задач).

Первая демонстрация


Вот основной фрагмент кода, который мы использовали в демонстрации Strada за пределами нашей небольшой команды 1-го февраля 2011-го.

<!DOCTYPE html>
<html>
<meta http-equiv="X-UA-Compatible" content="IE=9">
<head>
    <title>JavascriptWebClient</title>
    <script src="http://ajax.aspnetcdn.com/ajax/jquery/jquery-1.5.1.js" type="text/javascript"></script> 
</head>
<body>
    <h1>Employee Pay</h1>
    
    <h3><font color="#3AC1EF">▍SalesEmployee's are paid: <span id='salesEmployeePay'></span></font></h3>

    <div id='results'></div>

    <script type='text/strada'>
        extern var $;
        class Employee(string name, double basepay) {
          public double calculatePay() {
            return basepay;
          }
        }
        class SalesEmployee(string name, double basepay, double salesbonus) : Employee(name, basepay) {
          public string getName() { return name; }
          public double calculatePay() {
            return base.calculatePay() + salesbonus;
          }
        }
        var employee = new Employee('Bob', 1000);
        var salesEmployee = new SalesEmployee('Jim', 800, 400);
        $('#salesEmployeePay').html(salesEmployee.calculatePay());
    </script>

    <!-- Load the Strada compiler to processs text/strada tags -->
    <script src='StradaCompiler.js' type="text/javascript"></script>
</body>
</html>

Здесь имеется множество элементов, которые в итоге вошли в тот язык, который сейчас называется TypeScript.

  1. Неявный процесс применения типов в строках 28-30 без необходимости аннотирования.
  2. Классы — как способ описания реализации механизмов приложения и типов переменных. Классы удобны в простом JavaScript, но в TypeScript они играют более важную роль, позволяя, за один заход, описывать и реализацию, и тип, без необходимости выполнения двойной работы.
  3. Ключевое слово extern, которое позволяет прозрачно взаимодействовать с обычным JS-кодом и библиотеками — даже в том случае, если они не снабжены (или пока не снабжены) информацией о типах.

Однако, тут можно видеть и кое-что такое, что в итоге мы убрали из TypeScript.

  1. Синтаксис классов. Нам очень хотелось использовать более «функциональный» синтаксис классов, показанный здесь. Я, незадолго до этого, работал над F#, который использует похожий краткий синтаксис для классов. Кроме того, мы тогда обсуждали похожий синтаксис для C#. Надо отметить, что такой синтаксис хорошо подходил к популярному в то время стилю использования замыканий для представления состояний в «классах» JavaScript — вместо свойств объектов, как можно видеть в подходе, основанном на прототипах классов, которые как раз стандартизировались в ECMAScript. Эта тема была спорной практически до выпуска TypeScript.

    То, что происходило с классами в ECMAScript, шло в другом направлении, и хотя мы хотели сблизить TypeScript с будущими стандартами, нам не нравилось то, что так пришлось бы писать классы TypeScript поверх стандартных конструкций (в обычных ситуациях это привело бы к четырёхкратному повторению имени переменной). В итоге мы вывели на первый план соответствие стандартам. Тогда это было рискованно. Дело в том, что было не вполне понятно, какая судьба ждёт ES6. Может быть, он, как и ES4, так и не вышел бы. Однако, для нас главным было следовать основной цели TypeScript, который должен был представлять собой как можно более легковесную абстракцию над JavaScript, даже с учётом того, что JS, в своём развитии, перешёл от ES3 к ES5, затем — к ECMAScript2017 и будет развиваться дальше.
  2. Указание типов в виде префиксов. В вышеприведённом коде можно видеть нечто вроде string name вместо name: string — записи, которая используется в современном TypeScript. Сначала мы склонялись к этому синтаксису, так как мы использовали ключевое слово var для типа, который теперь называется any. Это позволяет сделать неявное приведение к типу any явным в выражениях вроде var x = …. Это, кроме того, походило на то, что недавно было сделано в C#, когда там появился неявный тип var. Но грамматика языка, предусматривающая помещение сведений о типе в данную позицию, ведёт к множеству проблем с парсингом. Кроме того, в этой области был ценный прецедент с языками, подобными JavaScript, которые использовали формат name: string (в первую очередь — это ActionScript и так и не вышедший ECMAScript4). В результате мы изменили использованный тут подход очень рано. В образцах кода, которые были у меня через два месяца после первой презентации, уже использовался синтаксис с двоеточием.
  3. Суперклассы. У нас была конструкция class Foo : Bar для суперклассов, в стиле C#. Позже мы перешли к использованию ключевого слова extends.
  4. Подход к компиляции скриптов. Вот ещё одна мелочь. Обратите внимание на то, что код располагается в блоке <script type="text/strada">, и в комментарии указано, что ответственным за компиляцию блоков text/strada, что называется, «на лету», является StradaCompiler.js. Мы тогда представляли себе, что TypeScript будут размещать непосредственно в HTML с помощью тегов <script>, вместо использования самостоятельного этапа сборки проекта, и этот встроенный код будет транспилироваться (и, хочется надеяться, кэшироваться) при загрузке страницы. В то время подобный подход был популярен в небольших проектах на CoffeeScript, но, обсудив это с достаточно большими командами разработчиков, мы обнаружили, что подобное непрактично для многих, если не сказать для большинства, реальных ситуаций. На практике подобные трудности, в конечном итоге, решались с использованием инструментов вроде WebPack, которые динамически перекомпилировали код, но представляли его браузеру в скомпилированном виде.

Надо сказать, что всё это — ещё цветочки. Гораздо интереснее было то, как мы заставили этот фрагмент кода работать.

Как это работало


У меня тогда был небольшой второстепенный проект по написанию JavaScript-интерпретатора на F# (нечто вроде средства для перехода с работы над F# к работе над JavaScript). Частью этого проекта был довольно стабильный JavaScript (ES5) парсер. Я выполнил некоторые модификации грамматики (их понадобилось на удивление немного) для поддержки новых конструкций, которые мы добавили в Strada. В основном это выглядело примерно так:

@@ -459,8 +466,12 @@ StatementList:
     | Statement                              { [$1]}
 // See 12.2
 VariableStatement:                           
-    | VAR VariableDeclarationList SEMICOLON  { VariableStatement(List.rev($2)) }
-    | VAR VariableDeclarationList            { VariableStatement(List.rev($2)) }
+    | Type VariableDeclarationList SEMICOLON  { VariableStatement([], false, $1,List.rev($2)) }
+    | Type VariableDeclarationList            { VariableStatement([], false, $1, List.rev($2)) }
+    | READONLY Type VariableDeclarationList SEMICOLON  { VariableStatement([], true, $2,List.rev($3)) }
+    | READONLY Type VariableDeclarationList            { VariableStatement([], true, $2, List.rev($3)) }
 // See 12.2
 VariableDeclarationList:
     | VariableDeclaration                                  { [$1]}

Между прочим, похоже, что в этой ранней версии у нас был readonly.

Я нашёл способ выводить результирующие AST как обычный JavaScript, но вместо того, чтобы отбрасывать типы, я конвертировал их в комментарии Closure Compiler. В результате конструкция получилась не особенно стабильной, но этого было достаточно для запуска демки!

После этого, что вполне логично, мы просто вызывали для получившегося кода Closure Compiler в виде средства для проверки типов. У Closure Compiler был (и всё ещё есть) удобный сервис на AppEngine, в результате мы просто передавали в виде POST-запроса наш код этому сервису и получали сведения об ошибках.

В те дни Silverlight (и Flash) ещё играли заметную роль в разработке клиентских приложений. И поэтому для того, чтобы запустить парсер (написанный на F#), мы разместили его на странице в виде приложения Silverlight, которое находило скрипты text/strada, парсило их, отправляло их Closure и сообщало об ошибках.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;
using System.Net;
using System.IO;
using System.Web;

namespace JavascriptWebClient.Web
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "ClosureCompile" in code, svc and config file together.
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    [ServiceBehavior(IncludeExceptionDetailInFaults=true)]
    public class ClosureCompile : IClosureCompile
    {
        public string DoWork(string srcText)
        {
            System.Diagnostics.Debug.WriteLine("Hello!");

            var request = WebRequest.Create("http://closure-compiler.appspot.com/compile");
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            var stream = request.GetRequestStream();
            // Send the post variables  
            StreamWriter writer = new StreamWriter(stream);
            writer.Write("output_format=xml");
            writer.Write("&output_info=compiled_code");
            writer.Write("&output_info=warnings");
            writer.Write("&output_info=errors");
            writer.Write("&output_info=statistics");
            writer.Write("&compilation_level=ADVANCED_OPTIMIZATIONS");
            writer.Write("&warning_level=verbose");
            writer.Write("&js_code=" + HttpUtility.UrlEncode(srcText));
            writer.Flush();
            writer.Close();
            var response = request.GetResponse();
            Stream responseStream = response.GetResponseStream();
            StreamReader reader = new StreamReader(responseStream);
            // get the result text  
            string result = reader.ReadToEnd();

            return result;
        }
    }
}

Сколько воспоминаний вызывает этот код…

В результате мы могли получать интерактивные сведения об ошибках при изменении содержимого тега <script> средствами инструментов разработчика, что давало впечатление того, будто тут работает фоновая система проверки типов. Это позволило нам продемонстрировать некоторые ключевые особенности, которых мы хотели достичь. Правда то, как всё это было сделано, не подходило даже для того, чтобы над проектом мог бы работать хотя бы один реальный программист.

Вот ещё один фрагмент кода, который мы тогда показали. Здесь вы можете видеть ещё некоторые интересные отличия от того, что в результате превратилось в TypeScript. Например — тип double вместо number (мы думали о том, чтобы добавить ещё и int, но это уводило нас в сторону от семантики JavaScript), и префикс I для интерфейсов, как принято в C#. Тут же есть и некоторые вещи, которые остались, вроде интерфейсов и возможности свободно смешивать поиск элементов в нетипизированных объектах и строго типизированные классы.

interface IHashtable
{
    double lookup(string i);
    void set(string i, double d);        
}

class Hashtable()
{
    var o = {};

    public double lookup(string i) 
    {
        return this.o[i];
    }
    public void set(string i, double d)
    {
        this.o[i] = d;
    }
}

interface IExpression {
    double evaluate(IHashtable vars);
}

class Constant(double value) : IExpression {
    public double evaluate(IHashtable vars) {
        return value;
    }
}

class VariableReference(string name) : IExpression {
    public double evaluate(IHashtable vars) {
        var value = vars.lookup(name);
        return value;
    }
}

class Operation(IExpression left, string op, IExpression right) : IExpression {
    public double evaluate(IHashtable vars) {
        double x = this.left.evaluate(vars);
        double y = this.right.evaluate(vars);
        switch (this.op) {
            case '+': return x + y;
            case '-': return x - y;
            case '*': return x * y;
            case '/': return x / y;
        }
    }
}

IExpression e = new Operation(new VariableReference("x"),'+',new Constant(3));

function f() { 
        IExpression e = new Operation( new VariableReference("x"), 
            '*', new Operation( new VariableReference("y"), '+', new Constant(2) )); 
        IHashtable vars = new Hashtable(); 
        vars.set("x", 3); 
        vars.set("y", 5);
        e.evaluate(vars); // Outputs "21" 
        vars.set("x", 1.5);
        vars.set("y", 9); 
        return e.evaluate(vars); // Outputs "16.5" 
    } 

f();

Итоги


Примерно через полтора года после первой демонстрации мы выпустили TypeScript. И, через 5 лет, TypeScript стал одним из самых быстрорастущих языков программирования в индустрии разработки ПО.

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

Поздравляю всех с юбилеем TypeScript. Благодарю команду разработчиков и сообщество TS, благодаря которым TypeScript — это то, что он есть, за потрясающую работу.

Уважаемые читатели! Пользуетесь ли вы TypeScript?