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

Original author: Preethi Kasireddy
  • Translation
  • Tutorial
Как разработчик 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, Калифорния
Продолжение: «Преимущества и недостатки статических типов»
Support the author
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 43

    0
    Зачем использовать статические типы в JavaScript?

    ну и зачем?

    • UFO just landed and posted this here
        0

        В вашей цитате описано отличие (которое и ежу понятно в чем заключается), но таки не написано зачем оно в JavaScript.
        Я не то, чтобы похливарить собрался, просто в статье с названием "Зачем использовать статические типы в JavaScript?" хотелось бы увидеть это "зачем".

        • UFO just landed and posted this here
            0

            Это был просто комментарий о несоответствии заголовка и самой статьи, если еще не поняли. Ну и насчет "проще программировать" — существуют и иные точки зрения, причем также вполне обоснованные.

              0

              Ну вроде как иметь дополнительную информацию о типах — это лучше, чем не иметь ее вообще, разве нет? Тем более, что она опциональна и, при необходимости, легко обходится.

                0

                Иметь эту информацию можно и не прибегая к использованию Flow или TS. И еще раз хочу уточнить: я тут не защищаю какую-то конкретную позицию относительно подхода к типизации или инструментов для работы с типами (позиция у меня есть, но, повторюсь, не хочу холиварить). Почему это не понятно после уже двух уточняющих комментов — для меня загадка.

                  0

                  Каким образом? Мне тоже было бы интересно убрать стадию транспиляции из джаваскрипта.


                  Я знаю единственный вариант — это использование JSDoc комментариев, однако их функционал — это 1% от функционала Тайпскрипта (насчет Флоу не знаю). Плюс JSDoc не дружит с ES6+.

                  • UFO just landed and posted this here
          +1

          На самом деле это удобно.
          Я очень долго скептически относился к белкам-истеричкам из лагеря Java и ей подобных (потому что там это действительно ужасно), но по факту с современными инструментами очень удобно писать (и читать чужой) явно типизированный JS.

            0
            Вангую второе рождение венгерской нотации.
              +2

              Как раз венгерская нотация — самая вредная и бесполезная хрень в этом деле. Кое-где можно встретить JS с ней, это ужасно.

                –1
                Это от неумения готовить.
                  0

                  Венгерская нотация сильно затрудняет читаемость кода, однако, если "ужасные" классические односимвольные префиксы заменить на постфиксы-сокращения (типа Arr, Str, Num), и использовать их только там, где необходимо сделать акцент на типе — вполне рабочая штука.

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

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

                        0
                        Я назвал их же. Мне ещё в ту эпоху попадалась статья самого Шимоньи о том, что мелкомягкие извратили саму суть и теперь трудно донести до рядовых программистов, что задумывалось оно не для того.

                        если вся эта метадата существует в виде суффиксов и постфиксов — так оно как-то логичнее и ближе к естественному человеческому языку.

                        Венгерский — не человеческий? :)

                        А по сути — я с Вами скорее согласен, чем нет. В конце концов это частный случай lowerCamelCase.
            +1

            Не совсем правильно называть это "статическими типами", это скорее аннотации типов. Статическая типизация гарантирует (ну или пытается) отсутствие ошибок типов в рантайме. Ни флоу, ни тайпскрипт этого не гарантируют.

            • UFO just landed and posted this here
              0
              Увы это все просто красивая обертка… Пишу на TS сейчас очень много, проверял затранспайленый код в JS, и я Вам скажу что ничего не поменяется. А если еще и под капот заглянуть то максимум который вы увидите это преобразование ваших переменных в типизированные, скажем `toUint32` и так далее. Но гарантии никакой.
                +1

                В статье и говорится, что это compile-time проверка, а не run-time.

                  –3
                  ну так и не надо тогда подносить это как «Новую эру в Javascript» и «Строготипизированный Javascript». Претензия не к автору статьи конкретно, но и к майкрософту тоже, которые позиционируют этот TypeScript как строгую типизацию в js, которой никогда нет и не было. Строгая типизация там бы была если бы после каждого присваивания там проверялось какому типу данных принадлежит значение. Вся ваша типизация ломается ровно тогда когда вы начинаете колбэки использовать. А колбэки это очень большая часть кода. А решается эта типизация простым человеческим наименованием переменных. Накипело уже с этой типизацией.
                    0
                    Вся ваша типизация ломается ровно тогда когда вы начинаете колбэки использовать

                    Видно, что вы даже не пробовали.


                    А решается эта типизация простым человеческим наименованием переменных.

                    Почитайте что Спольски пишет про венгерскую нотацию.

                      –1
                      >Видно, что вы даже не пробовали.
                      ну так поправьте меня. Да, я даже не стал смотреть на документацию после того как я посмотрел в песочнице во что он преобразует код.
                      document.addEventListener('DOMContentLoaded', this.contentLoadedHandler);
                      

                      Разве TypeScript сможет на этапе компиляции проверить что придет в аргументах функции «contentLoadedHandler»? Я ведь туда ничего явно не передаю.
                        +3

                        Для этого существуют .d.ts файлы, которые описывают сигнатуры библиотечных классов/методов.


                        Допустим, вот описание для document.addEventListener.
                        Здесь утверждается, что сигнатура коллбэка будет EventListenerOrEventListenerObject, который в свою очередь ссылается на EventListener:


                        interface EventListener {
                            (evt: Event): void;
                        }

                        Отсюда видно, что придет в аргументах функции: это объект типа Event

                          0
                          Вы не подходящий пример привели, у вас IDE как раз проверит типы и выдаст ошибку если нужно.
                          Вот скрин http://take.ms/pIYE4.
                            0

                            Вы не поверите...

                            • UFO just landed and posted this here
                      0

                      А могли с самого начала ActionScript внедрить в браузеры. Как вариант.

                        +1

                        Идея внедрить Web Assembly кажется более удачной

                          0

                          Возможно) Мой комментарий относился конкретно к статической типизации и всему такому.

                            0

                            Ну так с web assembly можно (теоретически) использовать любой язык компилируемый для llvm, со строгой типизацией или без.

                          +1
                          AS3, Haxe и TypeScript основаны на EcmaScript-262 4 edition.
                          JS сообщество его отклонила, как «очень сложное» и ушло в какую-то неведомую даль)
                          +1
                          Вопрос, а почему именно на JS понадобилась типизация?

                          Ruby, Python — нет статической типизации. Все пишут и вроде никто не топит за нее.
                          И IDE для этих языков есть и работают они хорошо.

                          PS пробовал и кофескрипт и тайпскрипт — не пошло.
                            0
                            привет вам из мира питона — http://mypy-lang.org/
                            возможность compile-time проверки типов с помощью аннотаций, поддерживаемых языком — один из больших плюсов третьей версии.
                              0
                              О mypy в курсе. Только вот основные фреймворки написаны таки на питоне. И никто не прибегал в них к типизации. Откройте код Торнадо, Джанго или Фласка. И использование этих фреймворков не заставляет типизировать код.

                              PS если я где ошибся, приведите линк. Так как больше полутора лет уже не пишу на питоне.
                                0
                                основные используемые сейчас фреймворки написаны сильно раньше принятия PEP-484 (он вышел лишь с питоном 3.5), так что отсутствие в них type hinting вполне понятно.

                                Вы правы, никто не заставляет типизировать код, тем не менее по моему опыту существуют проекты, где типизация сократила бы (и уже сокращает!) нужду в количестве юнит тестов, читаемость кода.
                                Да, возможно типичному джанго-сайту тайп хинтинг не нужен, а вот специализированному фреймворку поверх джанго (таких за мою карьеру пришлось писать 3 штуки) или большому вики-проекту с десятками тысяч LOC и тысячами тестов — весьма.

                                Естественно это лишь мой личный опыт, но даже он показывает что своя аудитория у этой фичи есть, как и TypeScript в js (к слову на последнем месте работы большой фронтендовый проект как раз на TypeScript переводили и в целом остались довольны).
                                  0
                                  s/ читаемость кода/ улучшает читаемость кода/

                                  Также, не совсем понимаю пассаж про «основные фреймворки написаны таки на питоне» — mypy это лишь статический анализатор, который работает с тайп хинтами python 3.5, это не отдельный интерпретатор. Вы можете встроить его в свой CI пайплайн, как тот же flake8 и он будет
                              • UFO just landed and posted this here
                                  0
                                  Пишу на Java (middle). Пишу на JS (senior). Проблем в JS с нехваткой типов не испытываю. Не могу принять это как аргумент.
                                  • UFO just landed and posted this here
                                  0

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


                                  WebAssembly — не панацея, поскольку неспособен работать с DOM, так что писать обвязку на JavaScript всё равно придётся.

                                  0
                                  Ссылки ведут на одну и туже статью — «Зачем использовать статические типы в JavaScript? (Преимущества и недостатки)»:
                                  Части 2 и 3. Преимущества и недостатки статических типов (с детальным примерами).
                                  Часть 4. Нужно ли использовать статические типы в JavaScript или нет?

                                  Объедините их, чтобы не сбивать с толку, что что-то пропущено. Я долго искала часть 4, пока не прочла всё)
                                  Или разбейте статью, как в оригинале.

                                  Only users with full accounts can post comments. Log in, please.