javascript

Зачем использовать статические типы в JavaScript? (Пример статической типизации на Flow)

  • четверг, 13 апреля 2017 г. в 03:14:45
https://habrahabr.ru/post/326304/
  • Семантика
  • Программирование
  • JavaScript


Как разработчик JavaScript вы можете целый день программировать, но не встретить ни одного статического типа. Так зачем думать об их изучении?

Ну, на самом деле изучение типов — это не просто упражнение для развития мышления. Если вы вложите некоторое время в изучение статических типов, их преимуществ, недостатков и примеров использования, это может чрезвычайно улучшить ваши навыки программирования.

Заинтересованы? Тогда вам повезло — именно об этом наша серия статей.

Во-первых, определение


Проще всего понять статические типы — это противопоставить их динамическим. Язык со статическими типами называют языком со статической типизацией. С другой стороны, язык с динамическими типами называют языком с динамической типизацией.

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

Здесь остаётся усвоить ещё одну концепцию: что означает «проверка типа»?

Чтобы понять, посмотрим на типы Java и JavaScript.

«Типы» относятся к определяемому типу данных.

Например, в Java вы устанавливаете boolean так:

boolean result = true;

У этого значения правильный тип, потому что аннотация boolean соответствует логическому значению, указанному в result, а не целому числу или чему-то ещё.

С другой стороны, если вы попытаетесь объявить:

boolean result = 123;

…то это не скомпилируется, потому что указан неправильный тип. Код явно обозначает результат как boolean, но устанавливает его в виде целого числа 123.

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

var result = true;

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

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

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

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

Это означает, что программа на языке с динамической типизацией (как JavaScript или Python) может скомпилироваться, даже если она содержит ошибки типов.

С другой стороны, если программа на языке со статической типизацией (как Scala или C++) содержит ошибки типов, она не пройдёт компиляцию, пока ошибки не будут исправлены.

Новая эра JavaScript


Поскольку JavaScript является языком с динамической типизацией, вы можете спокойно объявлять переменные, функции, объекты и что угодно, не объявляя тип.

var myString = "my string";

var myNumber = 777;

var myObject = {
  name: "Preethi",
  age: 26,
};

function add(x, y) {
  return x + y;
}

Удобно, но не всегда идеально. Вот почему недавно появились инструменты вроде Flow и TypeScript, которые дают разработчикам JavaScript *вариант* использования статических типов.

Flow — это open source библиотека для статической проверки типов, которую разработала и выпустила Facebook. Она позволяет постепенно добавлять типы в ваш код JavaScript.

TypeScript, с другой стороны, представляет собой надмножество, которое компилируется в JavaScript — хотя по ощущениям TypeScript похож на новый язык со статической типизацией сам по себе. То есть очень похож на JavaScript и не сложен в освоении.

В каждом случае если вы хотите использовать типы, то явно говорите инструменту, в каких файлах осуществлять проверку типов. В случае TypeScript вы делаете, создавая файлы с расширением .ts вместо .js. В случае Flow вы указываете в начале кода комментарий @flow.

Как только вы объявили, что хотите осуществить проверку типов в файле, то можете использовать соответствующий синтаксис для указания типов. Различие между инструментами в том, что Flow — это «контролёр» типов, а не компилятор, а TypeScript, с другой стороны, — это компилятор.

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

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

В остальных частях этой статьи будет рассмотрено:

Часть 1. Небольшое введение в синтаксис и язык Flow.

Части 2 и 3. Преимущества и недостатки статических типов (с детальным примерами).

Часть 4. Нужно ли использовать статические типы в JavaScript или нет?

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

Без лишних слов, давайте приступать!

Часть 1. Небольшое введение в синтаксис и язык Flow


Чтобы понять преимущества и недостатки статических типов, вам следует сначала изучить основы синтаксиса для статических типов с использованием Flow. Если вы никогда раньше не работали в языке со статической типизацией, может понадобиться некоторое время, чтобы привыкнуть к синтаксису.

Начнём с изучения, как добавлять типы к примитивам JavaScript, а также конструкциям вроде массивов, объектов, функций и т. д.

boolean


Этот тип в JavaScript описывает логические значения (true или false).

var isFetching: boolean = false;

Обратите внимание, что при указании типа всегда используется такой синтаксис:



number


Этот тип описывает числа с плавающей запятой по стандарту IEEE 754. В отличие от многих других языков программирования, JavaScript не выделяет разные типы чисел (вроде целых, коротких, длинных, с плавающей запятой). Вместо этого все числа всегда хранятся как числа двойной точности. Следовательно, вам нужен только один тип для описания любого числа.

number включает в себя Infinity и NaN.

var luckyNumber: number = 10;

var notSoLuckyNumber: number = NaN;

string


Этот тип соответствует строке.

var myName: string = 'Preethi';

null


Тип данных null в JavaScript.

var data: null = null;

void


Тип данных undefined в JavaScript.

var data: void = undefined;

Обратите внимание, что null и undefined воспринимаются по разному. Если вы попытаетесь написать:

var data: void = null;

/*------------------------FLOW ERROR------------------------*/
20: var data: void = null                     
                     ^ null. This type is incompatible with
20: var data: void = null
              ^ undefined

Flow выдаст ошибку, потому что тип предполагал тип undefined, а это не то же самое, что тип null.

Массив


Описывает массив JavaScript. Вы применяете синтаксис Array<T> для определения массива, элементы которого имеет некий тип <T>.

var messages: Array<string> = ['hello', 'world', '!'];

Обратите внимание, что мы заменили T на string. Это значит, что мы объявляем messages как массив строк.

Объект


Описывает объект JavaScript. Есть разные способы добавить типы к объектам.

Вы можете добавить типы, чтобы описать форму объекта:

var aboutMe: { name: string, age: number } = {
  name: 'Preethi',
  age: 26,
};

Можете определить объект как карту, в которой строкам присваиваются некие значения:

var namesAndCities: { [name: string]: string } = {
  Preethi: 'San Francisco',
  Vivian: 'Palo Alto',
};

Также можете определить объекту тип данных Object:

var someObject: Object = {};

someObject.name = {};
someObject.name.first = 'Preethi';
someObject.age = 26;

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

any


Он представляет собой буквально любой тип. Тип any эффективно избегает любых проверок, так что вам не следует использовать его без крайней необходимости (например, если вам требуется обойти проверку типов или нужен аварийный люк).

var iCanBeAnything:any = 'LALA' + 2; // 'LALA2'

Тип any может пригодиться, если вы используете стороннюю библиотеку, которая расширяет другие прототипы системы (вроде Object.prototype).

Например, если библиотека расширяет Object.prototype свойством doSomething:

Object.prototype.someProperty('something');

то вы можете получить ошибку:

41:   Object.prototype.someProperty('something')
                       ^^^^^^ property `someProperty`. Property not found in
41:   Object.prototype.someProperty('something')
      ^^^^^^^^^^^^ Object

Чтобы избежать этого, используем any:

(Object.prototype: any).someProperty('something'); // No errors!

Функции


Самый распространённый способ добавлять типы к функциям — это назначать типы передаваемым аргументам и (когда уместно) возвращаемому значению:

var calculateArea = (radius: number): number => {
  return 3.14 * radius * radius
};

Можно добавлять типы даже к функциям async (см. ниже) и генераторам:

async function amountExceedsPurchaseLimit(
  amount: number,
  getPurchaseLimit: () => Promise<number>
): Promise<boolean> {
  var limit = await getPurchaseLimit();

  return limit > amount;
}

Обратите внимание, что наш второй параметр getPurchaseLimit описан как функция, которая возвращает Promise. И функция amountExceedsPurchaseLimit тоже должна возвращать Promise, в соответствии с описанием.

Псевдонимы типов


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

type PaymentMethod = {
  id: number,
  name: string,
  limit: number,
};

Выше создан новый тип под названием PaymentMethod, свойства которого составлены из типов number и string.

Теперь, если хотите использовать PaymentMethod, можете написать:

var myPaypal: PaymentMethod = {
  id: 123456,
  name: 'Preethi Paypal',
  limit: 10000,
};

Также можно создавать псевдонимы для любых примитивов оборачивая лежащий в основе тип внутрь другого типа. Например, если хотите псевдонимы типов для Name и Email:

type Name = string;
type Email = string;

var myName: Name = 'Preethi';
var myEmail: Email = 'iam.preethi.k@gmail.com';

Поступая так, вы подчёркиваете, что Name и Email — это разные вещи, а не просто строки. Поскольку имя и почтовый адрес не очень заменяют друг друга, то теперь вы не спутаете их случайно.

Параметризованные типы


Параметризованные типы (Generics) — это уровень абстракции над самими типами. Что имеется в виду?

Давайте посмотрим:

type GenericObject<T> = { key: T };

var numberT: GenericObject<number> = { key: 123 };
var stringT: GenericObject<string> = { key: "Preethi" };
var arrayT: GenericObject<Array<number>> = { key: [1, 2, 3] }

Создана абстракция для типа T. Теперь можно использовать любой тип, какой захотите, для представления T. Для numberT наше T будет числом. А для arrayT, оно будет принадлежать типу Array<number>.

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

Maybe


Maybe позволяет нам установить тип для значения, которое потенциально может быть null или undefined. Для некоего T будет установлен тип T|void|null. Это означает, что оно может быть или T, или void, или null. Для установления типа maybe нужно добавить вопросительный знак перед определением типа:

var message: ?string = null;

Здесь мы говорим, что сообщение является либо строкой string, либо null, либо undefined.

Вы также можете использовать maybe для указания, что свойство объекта будет или некоего типа T, или undefined:

type Person = {
  firstName: string,
  middleInitial?: string,
  lastName: string,
};

Поместив вопросительный знак после имени свойства middleInitial, можно указать, что это необязательное поле.

Непересекающиеся множества


Ещё один мощный способ для представления моделей данных. Непересекающиеся множества полезны, если программе нужно обрабатывать разные типы данных одновременно. Другими словами, формат данных может зависеть от ситуации.

Расширим тип PaymentMethod из наших предыдущих примеров по параметризованным типам. Предположим, что в приложении у пользователя может быть один из трёх платёжных методов. В этом случае можно написать что-то вроде такого:

type Paypal = { id: number, type: 'Paypal' };
type CreditCard = { id: number, type: 'CreditCard' };
type Bank = { id: number, type: 'Bank' };

Затем вы можете установить тип PaymentMethod как непересекающееся множество их этих трёх вариантов.

type PaymentMethod = Paypal | CreditCard | Bank;

Теперь платёжный метод всегда будет одним из трёх вариантов. Множество назначается «непересекающимся» благодаря свойству type.

Далее во второй части вы увидите больше практических примеров непересекающихся множеств.

Итак, почти закончили. Скажем только о парочке других особенностей Flow, достойных упоминания.

1) Вывод типа: Flow старается вывести типы везде, где только можно. Эта функция активируется, когда контролёр типов способен автоматически вывести тип данных в выражении. Это помогает избежать излишних аннотаций.

Например, можно написать:

/* @flow */

class Rectangle {
  width: number;
  height: number;

  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  circumference() {
    return (this.width * 2) + (this.height * 2)
  }

  area() {
    return this.width * this.height;
  }
}

Хотя в этом классе нет типов, Flow способен адекватно проверить их:

var rectangle = new Rectangle(10, 4);

var area: string = rectangle.area();

// Flow errors
100: var area: string = rectangle.area();
                        ^^^^^^^^^^^^^^^^ number. This type is incompatible with
100: var area: string = rectangle.area();
               ^^^^^^ string

Здесь я попыталась установить area как string, но в определении класса Rectangle мы установили, что width и height являются числами. Соответственно, по определению функции area, она должна возвращать number. Пусть я не определяла явно типы для функции area, Flow нашёл ошибку.

Заметим одну вещь, что мейнтейнеры Flow рекомендуют при экспорте определения класса добавлять явные определения, чтобы потом было проще установить причину ошибок, когда класс не используется в локальном контексте.

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

Не хочу вдаваться в подробности, потому что это более продвинутая функция, о которой я надеюсь написать отдельную статью, но если есть желание изучить её, стоит изучить документацию.

Мы закончили с синтаксисом


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

С окончанием описания синтаксиса давайте перейдём, наконец, к интересной части: изучению преимуществ и недостатков использования типов!

Об авторе: Прити Касиредди (Preethi Kasireddy), сооснователь и ведущий инженер компании Sapien AI, Калифорния
Продолжение следует...