![](https://habrastorage.org/files/b81/b42/c72/b81b42c729ca4baca2957ab2708640bc.jpg)
Ну, на самом деле изучение типов — это не просто упражнение для развития мышления. Если вы вложите некоторое время в изучение статических типов, их преимуществ, недостатков и примеров использования, это может чрезвычайно улучшить ваши навыки программирования.
Заинтересованы? Тогда вам повезло — именно об этом наша серия статей.
Во-первых, определение
Проще всего понять статические типы — это противопоставить их динамическим. Язык со статическими типами называют языком со статической типизацией. С другой стороны, язык с динамическими типами называют языком с динамической типизацией.
Ключевое отличие в том, что языки со статической типизацией выполняют проверку типа во время компиляции, а языки с динамической типизацией выполняют проверку типа во время выполнения программы.
Здесь остаётся усвоить ещё одну концепцию: что означает «проверка типа»?
Чтобы понять, посмотрим на типы 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;
Обратите внимание, что при указании типа всегда используется такой синтаксис:
![](https://habrastorage.org/getpro/habr/post_images/05e/b9b/c79/05eb9bc795566d0055eb31648cb68140.png)
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, что вы всё делаете правильно.
Не хочу вдаваться в подробности, потому что это более продвинутая функция, о которой я надеюсь написать отдельную статью, но если есть желание изучить её, стоит изучить документацию.
Мы закончили с синтаксисом
Мы немало рассмотрели в первой части! Надеюсь, что этот поверхностный обзор был полезен и понятен. Если интересно копнуть глубже, то советую погрузиться в хорошо написанную документацию и изучать.
С окончанием описания синтаксиса давайте перейдём, наконец, к интересной части: изучению преимуществ и недостатков использования типов!
![](https://habrastorage.org/files/2f6/52a/c99/2f652ac99bf147f385b4fc45b2e22cf6.jpg)
Продолжение: «Преимущества и недостатки статических типов»
Комментарии (42)
indestructable
12.04.2017 14:17+1Не совсем правильно называть это "статическими типами", это скорее аннотации типов. Статическая типизация гарантирует (ну или пытается) отсутствие ошибок типов в рантайме. Ни флоу, ни тайпскрипт этого не гарантируют.
ValentineY
12.04.2017 16:40Увы это все просто красивая обертка… Пишу на TS сейчас очень много, проверял затранспайленый код в JS, и я Вам скажу что ничего не поменяется. А если еще и под капот заглянуть то максимум который вы увидите это преобразование ваших переменных в типизированные, скажем `toUint32` и так далее. Но гарантии никакой.
k12th
12.04.2017 16:54+1В статье и говорится, что это compile-time проверка, а не run-time.
aiband
12.04.2017 22:37-3ну так и не надо тогда подносить это как «Новую эру в Javascript» и «Строготипизированный Javascript». Претензия не к автору статьи конкретно, но и к майкрософту тоже, которые позиционируют этот TypeScript как строгую типизацию в js, которой никогда нет и не было. Строгая типизация там бы была если бы после каждого присваивания там проверялось какому типу данных принадлежит значение. Вся ваша типизация ломается ровно тогда когда вы начинаете колбэки использовать. А колбэки это очень большая часть кода. А решается эта типизация простым человеческим наименованием переменных. Накипело уже с этой типизацией.
k12th
12.04.2017 22:46Вся ваша типизация ломается ровно тогда когда вы начинаете колбэки использовать
Видно, что вы даже не пробовали.
А решается эта типизация простым человеческим наименованием переменных.
Почитайте что Спольски пишет про венгерскую нотацию.
aiband
12.04.2017 23:15-1>Видно, что вы даже не пробовали.
ну так поправьте меня. Да, я даже не стал смотреть на документацию после того как я посмотрел в песочнице во что он преобразует код.
document.addEventListener('DOMContentLoaded', this.contentLoadedHandler);
Разве TypeScript сможет на этапе компиляции проверить что придет в аргументах функции «contentLoadedHandler»? Я ведь туда ничего явно не передаю.amakhrov
13.04.2017 01:27+3Для этого существуют .d.ts файлы, которые описывают сигнатуры библиотечных классов/методов.
Допустим, вот описание для document.addEventListener.
Здесь утверждается, что сигнатура коллбэка будет EventListenerOrEventListenerObject, который в свою очередь ссылается на EventListener:
interface EventListener { (evt: Event): void; }
Отсюда видно, что придет в аргументах функции: это объект типа Event
Kayun
13.04.2017 09:18Вы не подходящий пример привели, у вас IDE как раз проверит типы и выдаст ошибку если нужно.
Вот скрин http://take.ms/pIYE4.
Alexeyco
13.04.2017 15:20На этапе компиляции это проверить очень просто.
1. Вижу использование функции
2. Смотрю тип параметров, которые в нее передаются в данном конкретном месте
3. Проверяю, не противоречат ли они объявленным
4. ???
5. Ошибка
franzose
13.04.2017 08:25А могли с самого начала ActionScript внедрить в браузеры. Как вариант.
k0t0vich
13.04.2017 16:55+1AS3, Haxe и TypeScript основаны на EcmaScript-262 4 edition.
JS сообщество его отклонила, как «очень сложное» и ушло в какую-то неведомую даль)
Aries_ua
13.04.2017 10:49+1Вопрос, а почему именно на JS понадобилась типизация?
Ruby, Python — нет статической типизации. Все пишут и вроде никто не топит за нее.
И IDE для этих языков есть и работают они хорошо.
PS пробовал и кофескрипт и тайпскрипт — не пошло.q210
13.04.2017 12:57привет вам из мира питона — http://mypy-lang.org/
возможность compile-time проверки типов с помощью аннотаций, поддерживаемых языком — один из больших плюсов третьей версии.Aries_ua
13.04.2017 22:30О mypy в курсе. Только вот основные фреймворки написаны таки на питоне. И никто не прибегал в них к типизации. Откройте код Торнадо, Джанго или Фласка. И использование этих фреймворков не заставляет типизировать код.
PS если я где ошибся, приведите линк. Так как больше полутора лет уже не пишу на питоне.q210
14.04.2017 14:29основные используемые сейчас фреймворки написаны сильно раньше принятия PEP-484 (он вышел лишь с питоном 3.5), так что отсутствие в них type hinting вполне понятно.
Вы правы, никто не заставляет типизировать код, тем не менее по моему опыту существуют проекты, где типизация сократила бы (и уже сокращает!) нужду в количестве юнит тестов, читаемость кода.
Да, возможно типичному джанго-сайту тайп хинтинг не нужен, а вот специализированному фреймворку поверх джанго (таких за мою карьеру пришлось писать 3 штуки) или большому вики-проекту с десятками тысяч LOC и тысячами тестов — весьма.
Естественно это лишь мой личный опыт, но даже он показывает что своя аудитория у этой фичи есть, как и TypeScript в js (к слову на последнем месте работы большой фронтендовый проект как раз на TypeScript переводили и в целом остались довольны).
q210
14.04.2017 14:38s/ читаемость кода/ улучшает читаемость кода/
Также, не совсем понимаю пассаж про «основные фреймворки написаны таки на питоне» — mypy это лишь статический анализатор, который работает с тайп хинтами python 3.5, это не отдельный интерпретатор. Вы можете встроить его в свой CI пайплайн, как тот же flake8 и он будет
Alexeyco
13.04.2017 15:23Вместо тысячи слов — просто советую попробовать поизучать C#, Java, Golang etc. Только увлеченно… и потом, после полугодика регулярных практик вы сами кому угодно расскажете, зачем строгая типизация (или на худой конец, тайпхинтинг).
darkdaskin
14.04.2017 15:02Если тот же Python не устраивает из-за отсутствия статической типизации, можно просто выбрать другой язык. А если нужно исполнять код в браузере, альтернатив JavaScript нет, вот и приходится жевать кактус. Вполне естественно желание некоторых разработчиков сделать его не таким колючим.
WebAssembly — не панацея, поскольку неспособен работать с DOM, так что писать обвязку на JavaScript всё равно придётся.
i360u
ну и зачем?
Alexeyco
Вроде же сказали. Еще один интересный плюс… это более прозрачная работа IDE. Если IDE знает, что возвращает/принимает функция/метод, это сильно упрощает процесс разработки. Автодополнения, выделение ошибочных участков красным. Еще даже до компиляции.
i360u
В вашей цитате описано отличие (которое и ежу понятно в чем заключается), но таки не написано зачем оно в JavaScript.
Я не то, чтобы похливарить собрался, просто в статье с названием "Зачем использовать статические типы в JavaScript?" хотелось бы увидеть это "зачем".
Alexeyco
Я даже растерялся. Ну чтобы было проще программировать. И чтобы ошибок было чуть-чуть меньше.
i360u
Это был просто комментарий о несоответствии заголовка и самой статьи, если еще не поняли. Ну и насчет "проще программировать" — существуют и иные точки зрения, причем также вполне обоснованные.
indestructable
Ну вроде как иметь дополнительную информацию о типах — это лучше, чем не иметь ее вообще, разве нет? Тем более, что она опциональна и, при необходимости, легко обходится.
i360u
Иметь эту информацию можно и не прибегая к использованию Flow или TS. И еще раз хочу уточнить: я тут не защищаю какую-то конкретную позицию относительно подхода к типизации или инструментов для работы с типами (позиция у меня есть, но, повторюсь, не хочу холиварить). Почему это не понятно после уже двух уточняющих комментов — для меня загадка.
indestructable
Каким образом? Мне тоже было бы интересно убрать стадию транспиляции из джаваскрипта.
Я знаю единственный вариант — это использование JSDoc комментариев, однако их функционал — это 1% от функционала Тайпскрипта (насчет Флоу не знаю). Плюс JSDoc не дружит с ES6+.
Alexeyco
А как можно в ванильный JS впихнуть тайпхинтинг?
k12th
На самом деле это удобно.
Я очень долго скептически относился к белкам-истеричкам из лагеря Java и ей подобных (потому что там это действительно ужасно), но по факту с современными инструментами очень удобно писать (и читать чужой) явно типизированный JS.
Zenitchik
Вангую второе рождение венгерской нотации.
k12th
Как раз венгерская нотация — самая вредная и бесполезная хрень в этом деле. Кое-где можно встретить JS с ней, это ужасно.
Zenitchik
Это от неумения готовить.
i360u
Венгерская нотация сильно затрудняет читаемость кода, однако, если "ужасные" классические односимвольные префиксы заменить на постфиксы-сокращения (типа Arr, Str, Num), и использовать их только там, где необходимо сделать акцент на типе — вполне рабочая штука.
Zenitchik
«Классический» префиксы — это извращённое понимание первоначальной идеи. Префикс должен обозначать смысл типа данных, а не его механику. Скажем, в физических расчётах я обозначаю единицы измерения.
i360u
Классическими я назвал те самые майкрософтовские префиксы, с которых все пошло. И мы ведь говорили о применении венгерской нотации именно в контексте типов. Ничего не вижу плохого в обозначении единиц измерения, но лично мне гораздо проще разбираться с кодом, если вся эта метадата существует в виде суффиксов и постфиксов — так оно как-то логичнее и ближе к естественному человеческому языку.
Zenitchik
Я назвал их же. Мне ещё в ту эпоху попадалась статья самого Шимоньи о том, что мелкомягкие извратили саму суть и теперь трудно донести до рядовых программистов, что задумывалось оно не для того.
Венгерский — не человеческий? :)
А по сути — я с Вами скорее согласен, чем нет. В конце концов это частный случай lowerCamelCase.