Статьи

Почему JavaScript нужен тип

 

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

Документация для людей и машин

Во-первых, я хочу показать, как аннотации типов могут использоваться как документация для людей и машин.

Давайте посмотрим на эту функцию 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;
  //...
}    

Это дает нам гораздо больше информации.

  • Первый аргумент этой функции — строка.
  • Аргумент настройки не является обязательным. Мы видим все параметры, которые можно передать в функцию, и не только их имена, но и их типы.
  • Функция возвращает объект JQueryXHR, и мы можем видеть его свойства и функции.

Это подводит нас к моему первому пункту.

Типы служат документацией для программиста.

Кроме того, эта информация обеспечивает расширенные возможности автозаполнения, навигации и рефакторинга. Наличие таких инструментов — почти требование для больших проектов. Без них программисты боятся изменить код, что делает практически невозможным масштабный рефакторинг. По словам Андерса Хейлсберга, вы можете создать большое приложение на языке с динамической типизацией, но вы не можете его поддерживать. Без продвинутых инструментов большая кодовая база всегда находится в состоянии только для чтения.

Типы включают расширенные инструменты.

Типы обеспечивают концептуальную основу для программиста

Хороший дизайн — это все о четко определенных интерфейсах. И гораздо проще выразить идею интерфейса на языке, который их поддерживает.

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

образ

Как видите, оба класса играют роль покупателя. Несмотря на то, что это чрезвычайно важно для приложения, понятие покупателя четко не выражено в коде. Там нет файла с именем purchaser.js. И в результате, кто-то может изменить код, чтобы упустить тот факт, что эта роль вообще существует.

function processPurchase(purchaser, details){
}

class User {
}

class ExternalSystem {
}

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

Now, compare it with a version where we explicitly define the Purchaser interface.

interface Purchaser {
  id: int;
  bankAccount: Account;
}

class User implements Purchaser {}
class ExternalSystem implements Purchaser {}

The typed version clearly states that we have the Purchaser interface, and the User and ExternalSystem classes implement it. This brings us to my next point.

Types are useful for defining interfaces/protocols/roles. They provide a conceptual framework for the programmer.

It is important to realize that types do not force us to introduce extra abstractions. The Purchaser abstraction is present in the JavaScript version of the code, but it is not explicitly defined.

I’ve spent most of my career programming in Ruby and JavaScript, and I find these languages extremely powerful and flexible. But after working on sizeable apps written in these languages, I came to the conclusion that the lack of interfaces pushes developers toward building tightly coupled systems.

In a statically typed language boundaries between subsystems are defined using interfaces. Since Ruby and JavaScript lack interfaces, boundaries are not well expressed in code. Not being able to clearly see the boundaries, developers start depending on concrete types instead of abstract interfaces. And it leads to tight coupling. It is possible to build large decoupled systems in these languages, but it requires a lot of discipline.

Statically Typed Languages?

It may seem that I am advocating statically typed languages, but I am not. The arguments against a mandatory static type system are still sound.

  1. It complicates the language. Writing a mock library in JavaScript is an exercise that almost everyone can undertake. Doing similar stuff in Java is far more difficult.
  2. It constrains the expressiveness of the language.
  3. It requires type annotations even when they are not desirable (e.g., in a domain specific language).

What we want is something that combines the power of a dynamically typed language with the benefits of having explicit interfaces and type annotations.

Optional Type Systems

An optional type system does not require using type annotations. Type annotations are used to help you communicate your intent and get better tooling.

Optional type systems are forgiving. You do not have to satisfy the type checker. Quite the opposite, the type checker is there to help you find typos, search, and refactor. You do not completely rewrite your program to make the types work. If you are prototyping something, and duck typing works, just do not use types. Use them only when they add value.

It would not be completely fair to say that optional typing is only good stuff without any drawbacks. It still adds some complexity to the language (nothing compared to a mandatory static type system though).Also, to fully see the benefits of such a system you need more sophisticated tools than a plain text editor. But because of the reasons I outlined in this blog post, I think it is worth paying this price.

Typed JavaScript

I wrote the original version of this blog post some time ago, and called it “I Wish Ruby Had Interfaces”. Back then I was a Rails developer. And I was extremely frustrated by working on a very large Rails code base, where some refactorings were taking months to finish. There was no hope Ruby would get an optional type system, so it was just a rant.

This time it is different. The JavaScript community is embracing the idea of optional types. TypeScript has become a robust tool used in production at many companies. AtScript, and Flow are new projects in this area. I am really excited to see what typed JavaScript community will look like in a few years.