Здравствуйте все, кто не забывает заглядывать в наш блог и в традиционно нерабочее время суток!
Давным-давно в нашей публикации от 13 ноября 2015 года вы убедили нас дождаться допиливания Angular 2 и издать о нем книгу. Мы всерьез собираемся взяться за такой проект в самое ближайшее время, а пока предлагаем почитать развернутый ответ на вопрос, вынесенный в заглавие этого поста.
Angular 2 написан на языке TypeScript. В этой статье я расскажу, почему было принято такое решение. Кроме того, поделюсь собственным опытом работы с TypeScript; каково на нем писать и рефакторить код.
Мне TypeScript по вкусу, а вам – возможно, и нет.
Да, Angular 2 написан на TypeScript, но приложения в Angular 2 можно писать и без него. Фреймворк отлично взаимодействует с ES5, ES6 и Dart.
В TypeScript – отличный инструментарий
Основное достоинство TypeScript – это его оснастка. Здесь обеспечивается сложное автозавершение, навигация и рефакторинг. В больших проектах без таких инструментов практически не обойтись. Своими силами вечно не решаешься окончательно изменить код, вся база кода пребывает в сыром виде, и любой крупный рефакторинг становится рискованным и затратным.
TypeScript – не единственный типизированный язык, который компилируется в JavaScript. Есть и другие языки с более строгими системами типов, которые теоретически должны предоставлять абсолютно феноменальный инструментарий. Но на практике в большинстве из них вы не найдете почти ничего кроме компилятора. Дело в том, что наработка богатого инструментария должна быть приоритетной целью с самого первого дня – именно такую цель и поставила перед собой команда TypeScript. Вот почему здесь были созданы языковые сервисы, которые могут использоваться в редакторах для проверки типов и автозавершения. Если вы задавались вопросом, откуда такое множество редакторов с отличной поддержкой TypeScript – дело именно в языковых сервисах.
Интеллектуальный ввод (intellisense) и простейший рефакторинг (например, переименование символа) коренным образом меняют процессы написания и, особенно, рефакторинга кода. Хотя этот показатель сложно измерить, мне кажется, что рефакторинги, на которые раньше тратилось несколько дней, теперь делаются за несколько часов.
Да, TypeScript значительно оптимизирует редактирование кода, но подготовка к разработке с ним сложнее чем, скажем, взять и забросить на страницу скрипт ES5. Кроме того, вы лишаетесь инструментов JavaScript для анализа исходного кода (напр., JSHint), но для них обычно есть адекватные замены.
TypeScript – это надмножество JavaScript
Поскольку TypeScript – это надмножество JavaScript, при миграции на этот язык не требуется радикально переписывать код. Это можно делать постепенно, модуль за модулем.
Просто берем модуль, переименовываем в нем файлы .js
в .ts
, а затем постепенно добавляем аннотации типов. Закончили с модулем – переходим к следующему. Когда вся база кода будет типизирована, можно начинать возиться с настройками компилятора, делать их строже.
Весь процесс может занять некоторое время, но, когда мы делали миграцию Angular 2 на TypeScript, в процессе работы удалось не только разработать новые функции, но и пофиксить баги.
В TypeScript абстракции становятся явными
Хороший дизайн – это грамотно определенные интерфейсы. А выразить идею интерфейса гораздо проще на языке, который интерфейсы поддерживает.
Допустим, есть приложение для покупки книг. Книги в нем приобретаются двумя способами: зарегистрированный пользователь может делать это через графический интерфейс, а прочие – через внешнюю систему, которая соединяется с приложением через некий API.
Как видите, роль обоих классов в данном случае – «покупатель». Несмотря на то, как важна роль «покупатель» в этом приложении, в коде она никак явно не выражена. Там нет файла purchaser.js
. Поэтому, кто-нибудь может изменить код и даже не заметить, что такая роль существует.
function processPurchase(purchaser, details){ }
class User { }
class ExternalSystem { }
Если просто просмотреть код, то сложно сказать, какие объекты могут выступать в роли покупателя. Наверняка мы не знаем, и наши инструменты не особенно нам в этом помогут. Такую информацию приходится выуживать вручную – а это дело медленное, чреватое ошибками.
А вот версия, в которой мы явно определяем интерфейс Purchaser
.
interface Purchaser {id: int; bankAccount: Account;}
class User implements Purchaser {}
class ExternalSystem implements Purchaser {}
В типизированной версии четко обозначено, что у нас есть интерфейс Purchaser
, а классы User
и ExternalSystem
его реализуют. Итак, интерфейсы TypeScript позволяют определять абстракции/протоколы/роли.
Важно понимать, что TypeScript не вынуждает нас добавлять лишние абстракции. Абстракция «Покупатель» есть и в коде JavaScript, просто она там явно не определена.
В статически типизированном языке границы между подсистемами определяются при помощи интерфейсов. Поскольку в JavaScript нет интерфейсов, обозначить границы на чистом JavaScript сложнее. Если границы для разработчика не очевидны, то он зависит от конкретных типов, а не от абстрактных интерфейсов, что провоцирует сильное связывание.
По опыту работы с Angular 2 до и после перехода на TypeScript такое убеждение лишь укрепилось. Определяя интерфейс, я вынужден продумывать границы API, это помогает мне очерчивать публичные интерфейсы подсистем и сразу выявлять связывание, если оно случайно возникнет.
С TypeScript проще читать и понимать код
Да, я в курсе, что на первый взгляд так не кажется. Тогда рассмотрим пример. Возьмем функцию jQuery.ajax()
. Какая информация понятна из ее сигнатуры?
Все, что можно сказать наверняка – эта функция принимает два аргумента. Типы можно попробовать угадать. Возможно, сначала идет строка, а за ней – конфигурационный объект. Но это всего лишь версия, возможно, мы ошибаемся. Не представляем, какие опции могут быть в объекте настроек (ни их имен, ни типов), не знаем, что возвращает эта функция.
Неизвестно, как вызвать данную функцию, надо свериться с исходным кодом или с документацией. Сверяться с исходным кодом – не лучший вариант; что толку в функциях и классах, если не понятно, как они реализованы. Иными словами, нужно опираться на их интерфейсы, а не на реализацию. Можно проверять документацию, но разработчики подтвердят, что это неблагодарный труд – на проверку тратится время, а сама документация зачастую уже неактуальна.
Итак, прочитать jQuery.ajax(url, settings)
просто, но, чтобы понять, как вызвать эту функцию, нужно вчитаться либо в ее реализацию, либо в документацию.
А теперь сравните с типизированной версией.
ajax(url: string, settings?: JQueryAjaxSettings): JQueryXHR;
interface JQueryAjaxSettings {
async?: boolean;
cache?: boolean;
contentType?: any;
headers?: { [key: string]: any; };
//...
}
interface JQueryXHR {
responseJSON?: any; //...
}
Эта версия гораздо информативнее:
- Первый аргумент этой функции – строка.
- Аргумент
settings
опционален. Мы видим все параметры, которые могут быть переданы функции – не только их имена, но и типы. - Функция возвращает объект
JQueryXHR
, мы видим его свойства и функции.
Типизированная сигнатура определенно длиннее нетипизированной, но :string
, :JQueryAjaxSettings
и JQueryXHR
– не мусор. Это важная «документация», благодаря которой код проще воспринимается. Можно понять код значительно глубже, не вдаваясь в реализацию или чтение документов. Лично я читаю типизированный код быстрее, потому что типы – это контекст, помогающий понимать код. Но, если кто-то из читателей найдет исследование о том, как типы влияют на удобочитаемость – оставьте, пожалуйста, ссылку в комментарии.
Одно из важных отличий между TypeScript и многими другими языками, компилируемыми в JavaScript – в том, что аннотации типов факультативны, и jQuery.ajax(url, settings) – это самый настоящий валидный TypeScript. Итак, типы в TypeScript можно сравнить скорее не с выключателем, а с регулировочным диском. Если вы полагаете, что код тривиален, и его вполне можно читать без аннотаций типов – не используйте их. Применяйте типы, только когда они приносят пользу.
TypeScript ограничивает выразительность?
Инструментарий в языках с динамической типизацией – так себе, но они пластичнее и выразительнее. Думаю, с TypeScript ваш код станет неповоротливее, но в значительно меньшей степени, чем может показаться. Сейчас поясню. Допустим, я использую ImmutableJS, чтобы определить запись Person
.
const PersonRecord = Record({name:null, age:null});
function createPerson(name, age) {
return new PersonRecord({name, age});
}
const p = createPerson("Jim", 44);
expect(p.name).toEqual("Jim");
Как типизировать такую запись? Для начала определим интерфейс под названием Person:
interface Person { name: string, age: number };
Если пытаемся сделать так:
function createPerson(name: string, age: number): Person {
return new PersonRecord({name, age});
}
то компилятор TypeScript ругается. Он не знает, что PersonRecord на самом деле совместим с Person, поскольку PersonRecord создавался рефлексивно. Некоторые читатели, знакомые с ФП, могут сказать: “Ах, если бы в TypeScript были зависимые типы!” Но здесь их нет. Система типов в TypeScript не самая продвинутая. Но наша цель иная: не доказать, что программа на 100% правильна, а предоставить вам более подробную информацию и более качественный инструментарий. Поэтому вполне можно срезать углы, если система типов не слишком гибкая.
Созданную запись можно запросто привести, вот так:
function createPerson(name: string, age: number): Person {
return <any>new PersonRecord({name, age});
}
Типизированный пример:
interface Person { name: string, age: number };
const PersonRecord = Record({name:null, age:null});
function createPerson(name: string, age: number): Person {
return <any>new PersonRecord({name, age});
}
const p = createPerson("Jim", 44);
expect(p.name).toEqual("Jim");
Это работает, потому что система типов структурна. Если в созданном объекте есть нужные поля — имя и возраст — то все в порядке.
Необходимо привыкнуть, что при работе с TypeScript «срезать углы» нормально. Только тогда вам будет приятно иметь дело с этим языком. Например, не пытайтесь добавлять типы в какой-нибудь причудливый код для метапрограммирования – скорее всего, статически это выразить просто не сможете. Поколдуйте с этим кодом и прикажите системе проверки типов, чтобы игнорировала вычурную часть. В таком случае выразительности вы почти не потеряете, но основная масса кода останется удобной для обработки и анализа.
Ситуация напоминает попытку обеспечить стопроцентное покрытие модульными тестами. 95% обычно делается без проблем, а вот добиться 100% — уже задача, причем, такое покрытие может негативно отразиться на архитектуре всего приложения.
При опциональной системе типов также сохраняется привычный для JavaScript цикл разработки. Крупные фрагменты базы кода, возможно, окажутся «разбиты», но вы все равно сможете их запускать. TypeScript так и будет генерировать JavaScript, даже если система проверки типов останется недовольна. В ходе разработки это исключительно удобно.
Почему TypeScript?
Сегодня у фронтендеров богатый выбор инструментов для разработки: ES5, ES6 (Babel), TypeScript, Dart, PureScript, Elm, т.д. Зачем же TypeScript?
Начнем с ES5. У ES5 есть одно серьезное преимущество над TypeScript: здесь не требуется транспилятор. Поэтому всю разработку организовать просто. Не приходится настраивать file watcher’ы, транспилировать код, генерировать карты кода. Все просто работает.
В ES6 нужен транспилятор, поэтому сама сборка будет организована примерно как в TypeScript. Но это стандарт, означающий, что все до одного редакторы или сборочные инструменты либо поддерживают ES6, либо будут поддерживать. В настоящий момент в большинстве редакторов TypeScript уже отлично поддерживается.
Elm и PureScript – красивые языки с мощными системами типов, которые могут дать вашей программе гораздо больше, чем TypeScript. Код на Elm и PureScript может получаться гораздо лаконичнее, чем на ES5.
У каждого из этих вариантов есть свои достоинства и недостатки, но мне кажется, что TypeScript – золотая середина, и отлично подойдет для большинства проектов. TypeScript обладает 95% достоинств хороших статически типизированных языков, и привносит эти достоинства в экосистему JavaScript. Ощущение почти такое же, как будто пишешь в ES6: пользуешься все той же стандартной библиотекой, теми же сторонними библиотеками, идиомами и многими привычными инструментами (например, разделом «Разработка» в Chrome). Вы получаете массу всего вкусного, не покидая привычной экосистемы JavaScript.
Автор: Издательский дом «Питер»