Мы о многом рассказали в первой части. Теперь с синтаксисом покончено, давайте наконец перейдём к самому интересному: изучению преимуществ и недостатков использования статических типов.Преимущество № 1: Вы можете заблаговременно находить баги и ошибки
Статическая проверка типов позволяет проверять, что определённый нами инвариант принимает значение
true, даже не запуская программу. И если имеется какое-то нарушение этих инвариантов, оно будет обнаружено перед запуском программы, а не во время её работы.Небольшой пример: предположим, что у нас небольшая функция, которая берёт радиус и вычисляет площадь:
const calculateArea = (radius) => 3.14 * radius * radius;
var area = calculateArea(3);
// 28.26Теперь если мы захотим передать функции радиус, который не является числом (типа «злоумышленник»)…
var area = calculateArea('im evil');
// NaNНам вернётся
NaN. Если какая-то функциональность основана на том, что функция calculateArea всегда возвращает число, то это приведёт к уязвимости или сбою. Не очень приятно, правда?Если бы мы использовали статические типы, то определили бы конкретный тип передаваемых параметров и возвращаемых значений для этой функции:
const calculateArea = (radius: number): number => 3.14 * radius * radius;Попробуйте теперь передать что-нибудь кроме числа функции
calculateArea — и Flow вернёт удобное и симпатичное сообщение:calculateArea('Im evil');
^^^^^^^^^^^^^^^^^^^^^^^^^ function call
calculateArea('Im evil');
^^^^^^^^^ string. This type is incompatible with
const calculateArea = (radius: number): number => 3.14 * radius * radius;
^^^^^^ numberТеперь у нас есть гарантия, что функция будет принимать только валидные числа на входе и возвращать результат только в виде валидн��х чисел.
Поскольку контролёр типов сообщает вам об ошибках прямо во время написания кода, это намного удобнее (и намного дешевле), чем поиск бага после того, как код отправлен заказчику.
Преимущество № 2: У вас появляется живая документация
Типы работают как живая, дышащая документация и для вас, и для других.
Чтобы понять каким образом, посмотрим на метод, который я однажды нашла в большой кодовой базе, с которой работала:
function calculatePayoutDate(quote, amount, paymentMethod) {
let payoutDate;
/* business logic */
return payoutDate;
}На первый взгляд (и на второй, и на третий), совершенно непонятно, как использовать эту функцию.
Является ли quote числом? Или логическим значением? Платёжный метод — это объект? Или это может быть строка, которая представляет тип платёжного метода? Возвращает ли функция дату в строковом виде? Или это объект
Date?Без понятия.
В то время я решила провести оценку всей деловой логики и делала grep по кодовой базе до тех пор, пока всё не выяснила. Но это слишком много работы только для того, чтобы понять, как работает простая функция.
С другой стороны, если бы написали что-то вроде такого:
function calculatePayoutDate(
quote: boolean,
amount: number,
paymentMethod: string): Date {
let payoutDate;
/* business logic */
return payoutDate;
}то немедленно стало бы ясно, какой тип данных функция принимает, а какой тип возвращает. Это пример того, как можно использовать статические типы для сообщения того, что функция намерена делать. Мы можем сообщить другим разработчикам, чего ожидаем от них, и можем увидеть, чего они ожидают от нас. Следующий раз, если кто-то соберётся использовать эту функцию, вопросов не возникнет.
Можно поспорить, что эта проблема решается добавлением комментариев к коду или документации:
/*
@function Determines the payout date for a purchase
@param {boolean} quote - Is this for a price quote?
@param {boolean} amount - Purchase amount
@param {string} paymentMethod - Type of payment method used for this purchase
*/
function calculatePayoutDate(quote, amount, paymentMethod) {
let payoutDate;
/* .... Business logic .... */
return payoutDate;
};Это работает. Но здесь гораздо больше слов. Кроме многословности, такие комментарии в коде трудно поддерживать, потому что они ненадёжные и не имеют структуры — некоторые разработчики пишут хорошие комментарии, а другие могут написать что-то непонятное, а некоторые вообще могут забыть оставить комментарий.
Особенно легко забыть обновить комментарий после рефакторинга. С другой стороны, у аннотаций типов чётко определённый синтаксис и структура, и они никогда не устареют, потому что закодированы в самом коде.
Преимущество № 3: Устраняется обработка запутанных ошибок
Типы помогают устранить обработку в коде запутанных ошибок. Давайте вернёмся к нашей функции
calculateArea и посмотрим, каким образом это происходит.На этот раз я передам ей массив радиусов для вычисления площадей для каждого радиуса:
const calculateAreas = (radii) => {
var areas = [];
for (let i = 0; i < radii.length; i++) {
areas[i] = PI * (radii[i] * radii[i]);
}
return areas;
};Эта функция работает, но неправильно обрабатывает некорректные входные аргументы. Если мы захотим убедиться, что функция правильно обрабатывает ситуации, когда входные аргументы не являются валидными массивами чисел, то придём к функции примерно такого вида:
const calculateAreas = (radii) => {
// Handle undefined or null input
if (!radii) {
throw new Error("Argument is missing");
}
// Handle non-array inputs
if (!Array.isArray(radii)) {
throw new Error("Argument must be an array");
}
var areas = [];
for (var i = 0; i < radii.length; i++) {
if (typeof radii[i] !== "number") {
throw new Error("Array must contain valid numbers only");
} else {
areas[i] = 3.14 * (radii[i] * radii[i]);
}
}
return areas;
};Ого. Тут много кода для такого маленького кусочка функциональности.
А со статическими типами мы просто напишем:
const calculateAreas = (radii: Array<number>): Array<number> => {
var areas = [];
for (var i = 0; i < radii.length; i++) {
areas[i] = 3.14 * (radii[i] * radii[i]);
}
return areas;
};Теперь функция действительно выглядит так, как она выглядела перед добавлением всего визуального мусора из-за обработки ошибок.
Легко понять преимущества статических типов, правда?
Преимущество № 4: Вы можете увереннее осуществлять рефакторинг
Объясню это историей из жизни. Однажды я работала с очень большой кодовой базой, и нужно было обновить метод, установленный в классе
User. В частности, изменить один из параметров функции со string на object.Я произвела изменение, но мне было страшно отправлять коммит — по всему коду разбросано так много вызовов к этой функции, что я не была уверена, что правильно обновила все экземпляры. Что если какой-то вызов остался где-то глубоко в непроверенном вспомогательном файле?
Единственный с��особ проверить — это отправить код и молиться, что он не взорвётся кучей ошибок.
При использовании статических типов такой проблемы не возникнет. Там у меня будет мир и спокойствие в душе: если я обновлю функцию и определения типов, то контролёр типов будет рядом и обнаружит все ошибки, которые я могла упустить. Останется только пройтись по этим ошибкам типов и исправить их.
Преимущество № 5: Разделение данных и поведения
Одно редко упоминаемое преимущество статических типов состоит в том, что они помогают отделяют данные от поведения.
Ещё раз посмотрим на нашу функцию
calculateArea со статическими типами:const calculateAreas = (radii: Array<number>): Array<number> => {
var areas = [];
for (var i = 0; i < radii.length; i++) {
areas[i] = 3.14 * (radii[i] * radii[i]);
}
return areas;
};Подумайте, как бы мы подошли к составлению этой функции. Поскольку мы указываем типы данных, то вынуждены в первую очередь думать о типах данных, которые собираемся использовать, чтобы можно было соответствующим образом установить типы для передаваемых параметров и возвращаемых значений.

Только после этого мы реализуем логику:

Возможность точно выразить данные отдельно от поведения позволяет явно указать наши предположения и более точно передать намерение, что снимает определённую ментальную нагрузку и даёт программисту определённую ясность ума. Иначе остаётся держать всё в уме каким-то образом.
Преимущество № 6: Устранение целой категории багов
Ошибки типов во время выполнения программы — одна из самых распространённых ошибок или багов, с которыми сталкиваются JavaScript-разработчики.
Например, предположим, что изначальное состояние приложения было установлено так:
var appState = {
isFetching: false,
messages: [],
};И предположим, что затем мы делаем вызов API, чтобы забрать сообщения и заполнить наш
appState. Далее, у нашего приложения есть чрезмерно упрощённый компонент для просмотра, который забирает messages (указанные в состоянии выше) и отображает количество непрочитанных сообщений и каждое сообщение как элемент списка:import Message from './Message';
const MyComponent = ({ messages }) => {
return (
<div>
<h1> You have { messages.length } unread messages </h1>
{ messages.map(message => <Message message={ message } /> )}
</div>
);
};Если вызов API для забора сообщений не сработал или вернул
undefined, то вы столкнётесь с ошибкой типа в продакшне:TypeError: Cannot read property ‘length’ of undefined…и ваша программа завершится со сбоем. Вы потеряете клиента. Занавес.
Посмотрим, как могут помочь статические типы. Начнём с добавления типов Flow к состоянию приложения. Я использую псевдоним типа
AppState для определения состояния:type AppState = {
isFetching: boolean,
messages: ?Array<string>
};
var appState: AppState = {
isFetching: false,
messages: null,
};Поскольку известно, что API для забора сообщений работают ненадёжно, то укажем для значения
messages тип maybe для массива строк.Так же как в прошлый раз, мы забираем сообщения через ненадёжный API и используем их в компоненте просмотра:
import Message from './Message';
const MyComponent = ({ messages }) => {
return (
<div>
<h1> You have { messages.length } unread messages </h1>
{ messages.map(message => <Message message={ message } /> )}
</div>
);
};Но в этот момент Flow обнаружит ошибку и пожалуется:
<h1> You have {messages.length} unread messages </h1>
^^^^^^ property `length`. Property cannot be accessed on possibly null value
<h1> You have {messages.length} unread messages </h1>
^^^^^^^^ null
<h1> You have {messages.length} unread messages </h1>
^^^^^^ property `length`. Property cannot be accessed on possibly undefined value
<h1> You have {messages.length} unread messages </h1>
^^^^^^^^ undefined
{ messages.map(message => <Message message={ message } /> )}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `map`. Method cannot be called on possibly null value
{ messages.map(message => <Message message={ message } /> )}
^^^^^^^^ null
{ messages.map(message => <Message message={ message } /> )}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `map`. Method cannot be called on possibly undefined value
{ messages.map(message => <Message message={ message } /> )}
^^^^^^^^ undefinedПогоди, приятель!
Поскольку мы определили
messages как тип maybe, мы разрешаем ему быть null или undefined. Но это не даёт нам права проводить операции с ним (вроде .length или .map) без осуществления проверки на null, потому что если значение messages на самом деле null или undefined, то выскочит ошибка типа при попытке проведения операции с ним.Так что вернёмся и обновим нашу функцию для просмотра примерно таким образом:
const MyComponent = ({ messages, isFetching }: AppState) => {
if (isFetching) {
return <div> Loading... </div>
} else if (messages === null || messages === undefined) {
return <div> Failed to load messages. Try again. </div>
} else {
return (
<div>
<h1> You have { messages.length } unread messages </h1>
{ messages.map(message => <Message message={ message } /> )}
</div>
);
}
};Теперь Flow знает, что мы учли все ситуации, где
messages равно null или undefined, так что проверка типов кода завершается с 0 ошибок. Прощайте, ошибки во время выполнения программы!Преимущество № 7: Уменьшение количества юнит-тестов
Раньше мы видели, как статические типы помогают избавиться от разбора запутанных ошибок, потому что они гарантируют типы передаваемых функции параметров и возвращаемых значений. Как следствие, статические типы также уменьшают количество юнит-тестов.
Например, вернёмся к нашей функции
calculateAreas с динамическими типами и обработкой ошибок.const calculateAreas = (radii) => {
// Handle undefined or null input
if (!radii) {
throw new Error("Argument is missing");
}
// Handle non-array inputs
if (!Array.isArray(radii)) {
throw new Error("Argument must be an array");
}
var areas = [];
for (var i = 0; i < radii.length; i++) {
if (typeof radii[i] !== "number") {
throw new Error("Array must contain valid numbers only");
} else {
areas[i] = 3.14 * (radii[i] * radii[i]);
}
}
return areas;
};Если бы мы были прилежными программистами, то могли бы подумать о тестировании недействительных передаваемых параметров для проверки, что они корректно обрабатываются нашей программой:
it('should not work - case 1', () => {
expect(() => calculateAreas([null, 1.2])).to.throw(Error);
});
it('should not work - case 2', () => {
expect(() => calculateAreas(undefined).to.throw(Error);
});
it('should not work - case 2', () => {
expect(() => calculateAreas('hello')).to.throw(Error);
});… и так далее. Но очень вероятно, что мы забудем протестировать какие-то граничные случаи, — и наш заказчик будет тем, кто обнаружит проблему. :(
Поскольку тесты основаны исключительно на ситуациях, какие мы придумали протестировать, то они экзистенциальные, а на практике их легко обойти.
С другой стороны, когда нам требуется установить типы:
const calculateAreas = (radii: Array<number>): Array<number> => {
var areas = [];
for (var i = 0; i < radii.length; i++) {
areas[i] = 3.14 * (radii[i] * radii[i]);
}
return areas;
};… мы не только получаем гарантию, что наша цель соответствует реальности, но такие тесты попросту надёжнее. В отличие от тестов на эмпирической основе, типы универсальны и их труднее обойти.
Если взглянуть в целом, то картина такова: тесты хороши для проверки логики, а типы — для проверки типов данных. Когда они сочетаются, то сумма частей даёт ещё больший эффект.
Преимущество № 8: Инструмент моделирования предметной области
Один из моих любимых примеров использования статичных типов — моделирование предметной области (domain modeling). В этом случае создаётся модель, которая включает в себя и данные, и поведение программы на этих данных. В данном случае лучше всего понять на примере, как использовать типы.
Предположим, что в приложении пользователю предлагается один или больше платёжных методов для совершения покупок на платформе. Пользователю разрешено выбрать из трёх платёжных методов (Paypal, кредитная карта, банковский счёт).
Итак, сначала применим псе��донимы типов для трёх платёжных методов:
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 Model = {
paymentMethods: Array<PaymentMethod>
};Это приемлемо? Ну, мы знаем, что для получения платёжных методов пользователя нужно сделать запрос к API и, в зависимости от результата и этапа процесса, приложение может принимать разные состояния. В реальности, возможно четыре состояния:
1) Мы не получили платёжные методы.
2) Мы в процессе получения платёжных методов.
3) Мы успешно получили платёжные методы.
4) Мы попытались получить платёжные методы, но возникла ошибка.
Но наш простой тип
Model с paymentMethods не покрывает все эти случаи. Вместо этого он предполагает, что paymentMethods всегда существует.Хм-м-м. Существует ли способ составить модель, чтобы состояние приложения принимало одно из этих четырёх значений, и только их? Давайте посмотрим:
type AppState<E, D>
= { type: 'NotFetched' }
| { type: 'Fetching' }
| { type: 'Failure', error: E }
| { type: 'Success', paymentMethods: Array<D> };Мы использовали тип непересекающегося множества для установки
AppState в одно из четырёх состояний, описанных выше. Заметьте, как я использую свойство type для определения, в каком из четырёх состояний находится приложение. Именно это свойство type и является тем, что создаёт непересекающееся множество. Используя его мы можем осуществить анализ и определить, когда у нас есть платёжные методы, а когда нет.Вы также заметите, что я передаю параметризованный тип
E и D в состояние приложения. Тип D будет представлять собой платёжный метод пользователя (PaymentMethod, определённый выше). Мы не установили тип E, который будет нашим типом для ошибки, так что сделаем это сейчас:
type HttpError = { id: string, message: string };Теперь можно смоделировать предметную область приложения:
type Model = AppState<HttpError, PaymentMethod>;В целом, подпись для состояния приложения теперь
AppState<E, D>, где E имеет форму HttpError, а D — это PaymentMethod. И у AppState есть четыре (и только эти четыре) возможных состояния: NotFetched, Fetching, Failure и Success.
Такие модели предметной области мне кажутся полезными для размышлений и разработки пользовательских интерфейсов в соответствии с определёнными бизнес-правилами. Бизнес-правила говорят нам, что приложение может быть только в одном из этих состояний, и это позволяет нам явно представить AppState и гарантировать, что оно будет только в одном из этих заранее установленных состояний. И когда мы разрабатываем по этой модели (например, создаём компонент для просмотра), становится абсолютно очевидно, что нужно обработать все четыре возможных состояния.
Более того, код документирует сам себя — достаточно посмотреть на непересекающиеся множества, и немедленно становится понятно, как структурирован AppState.
Недостатки использования статических типов
Как и всё остальное в жизни и программировании, проверка статических типов требует некоторых компромиссов.
Важно понять и признать эти недостатки, чтобы мы могли принять информированное решение, когда имеет смысл использовать статические типы, а когда они просто не стоят того.
Вот некоторые из этих соображений:
Недостаток № 1: Статические типы требуют предварительного изучения
Одна причина, почему JavaScript — такой фантастический язык для начинающих, состоит в том, что новичкам не требуется изучать полную систему типов перед началом продуктивной работы.
Когда я только выучила Elm (функциональный язык со статической типизацией), типы часто мешали. Я постоянно сталкивалась с ошибками компилятора из-за своих определений типов.
Изучение эффективного использования типов — это была половина успеха в изучении самого языка. В итоге, из-за статических типов кривая обучения Elm круче, чем у JavaScript.
Это особенно важно для начинающих, у которых максимально велика когнитивная нагрузка от изучения синтаксиса. Добавление синтаксиса в этот набор может сокрушить новичка.
Недостаток № 2: Можно увязнуть в многословии
Из-за статических типов программы часто выглядят более многословными и загромождёнными.
Например, вместо этого:
async function amountExceedsPurchaseLimit(amount, getPurchaseLimit){
var limit = await getPurchaseLimit();
return limit > amount;
}Нам приходится писать:
async function amountExceedsPurchaseLimit(
amount: number,
getPurchaseLimit: () => Promise<number>
): Promise<boolean> {
var limit = await getPurchaseLimit();
return limit > amount;
}А вместо этого:
var user = {
id: 123456,
name: 'Preethi',
city: 'San Francisco',
};Приходится писать такое:
type User = {
id: number,
name: string,
city: string,
};
var user: User = {
id: 123456,
name: 'Preethi',
city: 'San Francisco',
};Очевидно, что добавляются лишние строки кода. Но есть парочка аргументов против того, чтобы считать это недостатком.
Во-первых, как мы упомянули раньше, статические типы уничтожают целую категорию тестов. Некоторые разработчики могут посчитать это совершенно разумным компромиссом.
Во-вторых, как мы видели раньше, статические типы иногда могут устранить необходимость обработки сложных ошибок, и это, в свою очередь, значительно уменьшает загромождённость кода.
Сложно сказать, является ли многословность реальным аргументом против типов, но стóит держать его в уме.
Недостаток № 3: Требуется время для достижения мастерства в использовании типов
Требуется много времени и практики, чтобы научиться наилучшим образом выбирать типы в программе. Более того, выработка хорошего чутья на то, что стоит отслеживать статически, а что лучше оставить в динамическом виде, тоже требует аккуратного подхода, практики и опыта.
Например, один из подходов т��кой: закодировать критическиую бизнес-логику статическими типами, но оставить краткосрочные или неважные фрагменты логики динамическими, чтобы избежать излишней сложности.
Понять разницу бывает трудно, особенно если менее опытному разработчику приходится принимать решения на лету.
Недостаток № 4: Статические типы могут задержать быструю разработку
Как я упоминала ранее, я слегка споткнулась о типы, когда изучала Elm — особенно когда добавляла код или делала изменения в нём. Постоянно отвлекаясь на ошибки компилятора, трудно делать работу и чувствовать прогресс.
Здесь аргумент в том, что из-за проверки статических типов программист может слишком часто терять концентрацию — а вы знаете, концентрация является ключевым фактором для написания хорошей программы.
Дело не только в этом. Контролёры статических типов тоже не всегда идеальны. Иногда возникает ситуация, когда вы знаете что делать, а проверка типов вмешивается и мешает.
Уверена, что я упустила какие-то ещё недостатки, но это самые важные для меня.
Нужно использовать статические типы в JavaScript или нет?

Первыми языками программирования, которые я изучила, были JavaScript и Python, оба языка с динамической типизацией.
Но освоение статических типов добавило новое измерение в то, как я думаю о программировании. Например, хотя я считала постоянные сообщения об ошибках компилятора в Elm подавляющими в первое время, потом определение типов и «угождение компилятору» стало второй натурой и на самом деле улучшило мои навыки программирования. Вдобавок, нет ничего более освобождающего, чем умный робот, который говорит мне, что я делаю что-то не так и как это исправить.
Да, есть неизбежные компромиссы статических типов, такие как излишняя многословность и необходимость тратить время на их изучение. Но типы добавляют безопасность и корректность в программы, что устраняет значимость этих «недостатков» для меня лично.
Динамические типы кажутся более быстрыми и простыми, но они могут подвести, когда вы реально запускаете программу в деле. В то же время можете поговорить с любым Java-разработчиком, который имеет дело с более сложными параметризованными типам — и он скажет, как сильно их ненавидит.
В конечном счёте, здесь нет универсального решения. Лично я предпочитают использовать статические типы при следующих условиях:
- Программа критически важна для вашего бизнеса.
- Программа, вероятно, подвергнется рефакторингу, в соответствии с новыми потребностями.
- Программа сложна и имеет много подвижных частей.
- Программу поддерживает большая группа разработчиков, которым нужно быстро и точно понять код.
С другой стороны, я бы отказалась от статических типов в следующих условиях:
- Код недолговечный и не является критически важным.
- Вы делаете прототип и стараетесь продвигаться как можно быстрее.
- Программа маленькая и/или простая.
- Вы единственный разработчик.
Преимущество разработки на JavaScript в наши дни состоит в том, что благодаря и��струментам вроде Flow и TypeScript у нас наконец-то появился выбор — использовать статические типы или старый добрый JavaScript.
Заключение
Надеюсь, эти статьи помогли вам понять важность типов, как их использовать и, самое главное, *когда* их использовать.
Возможность переключаться между динамическими и статическими типами — это мощный инструмент для JavaScript-сообщества, и захватывающий :)
Об авторе: Прити Касиредди (Preethi Kasireddy), сооснователь и ведущий инженер компании Sapien AI, Калифорния