×

«Строгий» JavaScript: зачем и кому это надо

Елена Жукова, предприниматель и Frontend developer, на VinnytsiaJS выступила с докладом «Strict JavaScript» и на его основе написала статью для DOU. JavaScript считается динамическим языком, но все чаще используются инструменты, которые добавляют ему статической типизации. Google, Facebook и Microsoft предлагают свои решения. Почему так происходит и стоит ли это делать?

Данные из GitHub за последние 3 года показывают существенный рост количества проектов, написанных с помощью инструментов, которые добавляют в JavaScript статической типизации. Конечно, можно предположить, что использованию, например, TypeScript способствовал выход фреймворка Angular2, который настоятельно рекомендовал использовать именно TypeScript, однако существует немало проектов, которые используют совершенно другой фреймворк — ReactJS, при этом написаны на TypeScript. К тому же Facebook разработал собственный инструмент для статической типизации JavaScript — Flow, которым рекомендует пользоваться для своего фреймворка ReactJS.

Такое широкое распространение статических надстроек над динамическим JavaScript-ом заставляет задуматься: «Зачем и кому это надо?».

Небольшой экскурс в историю появления JavaScript

Мы все знаем, что JavaScript был создан в 1995 году Бренданом Эйхом для браузера Нетскейп навигатор. Автор говорил, что на его работу повлияли такие популярные в то время языки программирования как Scheme, Self, Java и Smalltalk. Примечательно, что все эти языки имеют строгую типизацию. Так почему же JavaScript получился таким слабо типизированным?

Возможно, цитата автора поможет это понять:

«HTML нуждался в „языке сценариев“, языке программирования, который был бы прост в использовании любителями и новичками, где код мог быть написан непосредственно в исходной форме как часть разметки веб-страницы. Мы стремились предоставить „язык-прослойку“ для веб-дизайнеров и программистов-любителей, которые строят веб-контент из таких компонентов, как изображения, плагины и апплеты Java. Мы рассматривали Java как „компонентный язык“, используемый более опытными программистами, тогда как программисты прослойки — разработчики веб-страниц — собирали бы компоненты и автоматизировали бы их взаимодействия с помощью JS».

Брендан сильно бы удивился, если бы ему тогда сказали, что пройдет 20 лет, и все браузеры откажутся от использования Java апплетов, а JavaScript станет самым популярным языком.

JavaScript оставался несерьезным языком для веб-дизайнеров до 2004 года, когда Google, используя технологию AJAX, выпустили в массовое потребление Gmail. Для своего почтовика Google уже не использовали чистый JS. Они придумали набор инструментов Google Web Toolkit, который преобразовывал Java в JavaScript. Основной целью GWT заявлял кроссбраузерность, свободу от изучения JavaScript, а также общий код для бэкенда и фронтенда (single code base).

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

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

В 2011 году появился TypeScript — относительно самостоятельный язык (под сильным влиянием C#) специально для JavaScript-разработчиков, главной задачей которого была именно статическая типизация для JS. В 2014 подобное решение — Flow — представили и в Facebook.

Аргументы «за»

Так почему же гиганты индустрии Google, Microsoft и Facebook занимаются одним делом — добавляют статической или строгой типизации JavaScript?

Здесь можно указать на несколько причин.

Найти ошибки на этапе компиляции

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

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

/*
* JavaScript coercion
*/
var objArr = {}+[];

var arrObj = []+{};

var arrNum = []+1;

var arrNum = []*1;

var arrNum1 = [2]*2;

var arrNum2 = [2,1]+2;

var arrNum3 = [2,1]-2;

var arrs = [1]+[2];

В таких случаях хотелось бы получать сообщение об ошибке уже в процессе написания кода даже в самом простом редакторе, как это позволяет TypeScript или Flow с бесплатным редактором Atom.

Более читаемый код

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

Вот простой пример кода. Достаточно простой, чтобы разработчик решил, что здесь все предельно понятно: есть продукт, у него есть категория, зачем тут указывать типы?

function Product(){
}

Product.prototype.setCategory = function (category) {
  this.category = category;
};

Однако когда коллега автора этого кода или сам автор спустя некоторое время решает написать функцию поиска продукта по категории:

function searchProductsByCategory(products, category) {
  return products.filter(product=>{
    return //???
  })
}

Здесь возникает проблема: «А что такое категория? И как написать сравнение?». Может быть, что категория — это строка, или число, или объект...

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

interface Category{
  name: string;
  id: string;
}

class Product{
  private category:Category;
  setCategory(category:Category){
    this.category = category;
  }
}

Код, которым легче управлять

Изменения в коде — обычная рутина для программиста. Очень важно, чтобы эта работа происходила максимально безболезненно, но JavaScript — не тот случай. При достаточно большом объеме кода, особенно разделенном на несколько файлов, или слишком общих названиях переменных изменения в коде могут быть задачей очень нетривиальной. Рефакторинг становится значительно проще, если использовать TypeScript или Flow. Ваш редактор может автоматически изменить название переменной или метода во всех местах, где вы их используете.

Аргументы «против»

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

В предисловии к книге Кайла Симпсона «Вы не знаете JavaScript: типы и грамматика» Дэвид Волш пишет:

«JavaScript — единственный язык, который разработчики не учат, перед тем как начать использовать».

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

Например, Эрик Эллиот говорит:

«В динамически типизированном языке нет необходимости в конструкторах типов... Вместо этого разработчики могут использовать duck typing и, возможно, выполнять проверки типа времени выполнения».

В качестве примера Эллиот предлагает такую функцию для проверки наличия обязательных параметров:

const fn = (fn, {required = []}) => (params = {}) => {
  const missing = required.filter(param => !(param in params));

  if (missing.length) {
    throw new Error(`${ fn.name }() Missing required parameter(s):
    ${ missing.join(', ') }`);
  }

  return fn(params);
};

const createEmployee = fn(
  ({
    name = '',
    hireDate = Date.now(),
    title = 'Worker Drone'
  } = {}) => ({
    name, hireDate, title
  }),
  {
    required: ['name']
  }
);

console.log(createEmployee({ name: 'foo' })); // works
createEmployee(); // createEmployee() Missing required parameter(s): name

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

Дуглас Крокфорд, автор знаменитой книги «JavaScript. Сильные стороны», так отзывается о статических типах в JS:

«Я нашел в своей работе, что ошибки, которые обнаруживает строгая проверка типов, не являются ошибками, о которых я беспокоюсь. С другой стороны, я нахожу свободную типизацию либеральной. Мне не нужно создавать сложные иерархии классов. И мне никогда не приходится преобразовывать или бороться с системой типов, чтобы получить то поведение, которое я хочу».

Для примера, Крокфорд считает, что код, как этот, не соответствует духу JavaScript:

var answer = 42.5 | 0;

И предлагает такую конструкцию для того, чтобы преобразовать число с запятой (float) в целое число (int):

Number.method('integer', function (  ) {
    return Math[this < 0 ? 'ceil' : 'floor'](this);
});

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

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному2
LinkedIn

Схожі статті




41 коментар

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.

Больше всего печалит, что тайпскрипт уже набрал достаточный процент юзеров и теперь авторы библиотек просто обязаны добавлять поддержку тайпскрипта тайпингами или переписывать на тайпскрипте. А это даст мелкомягким возможность сказать что-то типа: «Вот, наш хайпскрипт такой классный, что все на нем пишут».

Там у мелкомягких появились какие-то чуваки, которые лазят по гитхабу и прикручивают везде тайпинги. Ну и если не вникать, то кажется , что все «серьезные дяди» поддерживают тайпскрипт. Хорошо изучил этот вопрос и пришел к выводам, что тайпскрипт — это хорошо, если его пишет кто-то , а не ты, к чему-то статическому и тебе в код этого чего-то заглядывать не придется (по крайней мере без идеешки). Например, есть либа, апи которой меняться не будет, к ней приделываются тайпинги и вуаля, в идешке начинают выскакивать красивые подсказочки. Ну и сишарповцам, похоже, помогает. Впринципе логично, чувак себе кодил в визуал студио, у него в каких-то случаях подкрашивало красным опечатки, а тут вдруг перестало подкрашивать. А есть люди , которые уже давно решили и забыли про такие проблемы в нормальном стандартном джаваскрипте и им вот этот весь хайп и путаница мешает и раздражает.

Сейчас как раз и решаем — использовать типизацию или нет.
Склоняемся к тому, что надо использовать.

Плюсы типизации:
1. уменьшаем риски багов (до 15%)
2. облегчаем переиспользование за счет подсказок со стороны IDE

Минусы:
1. Больше кода
2. для утилит (где много дженериков) гораздо больше кода
3. необходимость добавления .js.flow, .d.ts для старого кода

Очень хорошая статья в отношении того, типизировать или не типизировать:
blog.acolyer.org/...​table-bugs-in-javascript

var answer = 42.5 | 0;
не соответствует духу JavaScript:

Дак а кто такое юзает? это всегда было медленнее, да еще и в int32 загоняет...
Нужно проверять параметры на входе, как это и задумывалось. Если юзать jsdoc то IDE и так в большинстве случаев предупредит и подскажет простые кейсы при статическом анализе, но люди постоянно хотят усложнить еще одним слоем вундервафли.

Int31 :)

ну тогда уже (unsigned int 31) — 1 :)

як на мене, TypeScript(Flow) — дуже класні штуки для developer experience. Проте, вони не врятують тебе від багів на продакшні, бо джаваскріпт завжди компілюється ©. Тому не потрібно відноситись до них, як до чарівної палички, і все буде добре.

И внезапно статья обрывается)

Понравилась чья-то цитата, что в мире есть только несколько людей, который понимают, как работает JavaScript

Microsoft запилила TypeScript для школьников, для которых даже Javascript оказался слишком сложным

В JavaScript значно вищий поріг входження, ніж у той же Python чи Ruby.

только почему-то соотношение вакансий/откликов на

Python

меньше, а на

Ruby.

еще меньше ,чем на

JavaScript

Обманчиво низкий порог входа и «полезные» советы всем вайтишникам «иди в веб, там легко» делают своё дело. Хотя легко только хеллоуворлды писать, как впрочем в большинстве языков. А вот колбеки и замыкания уже не все могут понять. Про промисы я вообще молчу — взрыв мозга для вайтишника. И слабая динамическая типизация требует некоторого опыта чтобы правильно её готовить, хотя может казаться что js легче.

... а звали этого ниасилившего школьника ̶А̶л̶ь̶б̶е̶р̶т̶ ̶Э̶й̶н̶ш̶т̶е̶й̶н̶ Андерс Хейлсберг, бгг.

Ощущение, что примеры альтернатив были специально подобраны для демонтстрации «как всё плохо». Например для преобразования числа в целое есть Math.round, Math.floor и Math.ceil. В примере же показано удаление дробной части (такая функция есть, но она пока ещё молода и её нет в IE developer.mozilla.org/...​Global_Objects/Math/trunc), для чего некий Крокфорд очевидно предлагает добавить новый метод (кстати что такое Number.method наверное знает только он, я с таким не сталкивался и MDN о таком молчит). И что из этого следует? А ничего не следует. Понадобилась новая функция — добавил. Код этой функции каждый имплементит как хочет. В дальнейшем он будет вызывать просто эту функцию и всё.

Для проверки параметров тоже бывают разные методы. Самый простой, прямой и понятный:

function assert(message, test) {
  if (!test) {
    throw new Error(message);
  }
}

function myFunc(strVar) {
  assert("Parameter must be a string", typeof strVar === "string");

  return strVar.toUpperCase();
}

А чтобы тип прямо в определении функции был и IDE понимала, есть jsDocs.

Можете назвать IDE, которые понимают jsdoc полностью?

phpStorm понимает. Насколько полностью — не знаю, я не особо часто пользуюсь jsdoc. Вот недавно я проверял насколько это всё реализовано dou.ua/...​rums/topic/21728/#1185067

если вы в контексте статической типизации vs парсинга jsdoc, то такие штуки как @fires, @listen, @async etc все равно бы не покрывались типизацией.
@lends, @param, @returns работают и ок(про @mixes не уверен).

Обожнюю динамічну типізацію. Вона набагато ближча до аналогового світу, ніж статична.

З точки зору динамічної типізації ти нічим не відрізняєся від шлюхи для любителя гомосексуальних розваг.
З точки зору суворої типізації, щось підкаже що рантайм эксепшен при спробі буде вельми болісним.

Ви настільки боїтеся порушувати власновигадані правила, що навіть побудували навколо цього цілий культ. І якщо з’являються люди, які не є служителями цього культу, вони об’являються єретиками та негайно тягнуться на багаття. Розробка через ексепшини та можливості IDE — ось де справжній біль. Ви витрачаєте нереальну купу часу на танці, які нікому не потрібні, ефективність яких прямує до нуля. Але ви не бачите це, бо інакше ви станете єретиком та вже вас будуть тягти на багаття.

Процеси реального світу дуже погано описуються строгими правилами. Або описати можна із нереальними ускладненнями абстракцій. Динамічна типізація дозволяє концентруватися на процесах, а не на виконання умов мови або IDE. А це і є сутність бізнес-процесів, які треба реалізовувати в коді.

возвращаемся к теме «чем программист отличается от софтварь инжинера»

яке взагалі відношення статична/динамічна типізація має до бізнес-процесів ?
і чому бізнес-процеси раптом стали «процесами реального світу» ?

яке взагалі відношення статична/динамічна типізація має до бізнес-процесів ?

А що ми реалізовуємо щодня? Певні процеси. А з чого складається бізнес-процес? З процесів. А навіщо ми це робимо? Щоб полегшити комусь життя. Динамічна типізація дозволяє швидше описувати процеси та безболісніше їх міняти на ходу, в залежності від ситуації в реальному світі.

і чому бізнес-процеси раптом стали «процесами реального світу» ?

Приведіть приклад бізнес-процесу не з реального світу. Щось мені підказує, що зараз знову прийдеться довго й вперто пояснювати різницю між процесами та бізнес-процесами. :D

Аналоговий світ — це те, що вивчає фізика і хімія. Реальний аналоговий процес — це, скажімо, процес дифузії. В цьому плані опис даного реального фізичного процесу на мові програмування фактично є описом абстрактного процесу взаємодії між об’єктами моделі фізичного світу. При цьому модель і процеси, що описують взаємодію між її об’єктами є тільки якимось наближенням до реальності і з аналоговою інформацією напряму не працюють. Що ви ніби і не заперечуєте.
Йдемо далі — є процеси, які взагалі не мають відношення до фізичного світу. Опис якоїсь транзакції на біржі, чи виставлення вам рахунку — чиста абстракція. В цьому плані бізнес-процеси описують процеси, які є абстракціями і самі є абстракціями, що описують взаємодію між елементами domain model.
Максимум, куди можна приплести динамічну чи статичну типізацію — це наскільки точно можна описати якийсь абстрактний процес суто за допомогою типізації, не користуючись іншими можливостями перевірки вхідних/вихідних даних.
В цьому плані в JS маємо динамічну слабку неявну типізацію, яка занадто широко описує вхідні дані і роботу з ними. Умовно кажучи, в бізнес-процесі покупки вами квитка на літак один з процесів очікує отримання на вхід даних щодо підтвердження блокування суми на карті. І у нас це true/false, але фактично він може приймати на вхід що завгодно (бо JS). Що він потім з тим мусором, який отримає, буде робити — хз. Перевіряти прийдеться ручками. О так, може це не мусор, може в результаті змін на стороні банку йому на вхід замість true/false став приходити об’єкт, в якому true/false — тільки одне з полів, і є ще куча іншої інформації. А ми перепишемо тільки вміст, а сам опис інтерфейсів не чіпатимемо. От тільки якщо хтось на цей інтерфейс гляне — він хрен зрозуміє, що ваш процес приймає на вхід, не заглянувши всередину.

Для полного веселья такое же можно замутить и со строгой типизацией

Це з серії «вистрілити собі в ногу»

Например распарсить json в map?

Аналоговий світ — це те, що вивчає фізика і хімія.

Щось ви якось обмежено дивитеся на світ. Але нехай буде так для спрощення.
Так ось, ви тільки що намагалися спростити пояснення процесів аналогового світу до того рівня абстракцій, якими ви оперуєте. Але при спрощенні завжди втрачаються деталі. Давайте я вам приведу інший приклад. Людина підписала якийсь абстрактний договір. А тепер опишемо всі варіанти розвитку подій, які можуть трапитися з людиною та договором після цього підписання. Ой, та їх може бути мільярд. Щоб вивести навіть спрощену модель, наприклад з 5-ти основних подій, ви закопаєтеся в рівнях абстракцій. А тепер почнемо все це модифікувати та розширювати. Ой, все.

Опис якоїсь транзакції на біржі, чи виставлення вам рахунку — чиста абстракція.

Ви трохи не дочитали мій допис. Транзакція на біржі — реалізація певних подій в аналоговому світі. А саме — реалізація певного бізнес-процесу. Саме через нього виникла біржа та транзакції в ній. Навіть створення інтернет-сайту не є стовідсотковою цифровою подією.

Так ось, трохи кришталізую думку. Є процеси, саме їх автоматизація приносить найбільшу користь. Не опис об’єктів та процесів в об’єктно-орієнтованій нотації. Коли ви автоматизуєте процес, то чим більше у вас обмежень, тим менш гнучкою вийде ваша система. Наприклад, exception-less архітектура більш пристосована до життя, ніж exception-full. Що це значить? Що коли ви закладаєте повний контроль над процесом виконання коду, ви фактично бетонуєте певний стан. Вам здається, що процес виконання передбачуваний та контрольований. Але це примарний контроль. Із зростанням системи ви більше не можете контролювати все. І коли попадає піщинка в цей механізм, він ламається. Якщо ви закладаєте в систему поламки як частину цієї системи, вона дуже легко адаптується до будь-яких змін. Сам код стає універсальним настільки, наскільки можливо.

Що він потім з тим мусором, який отримає, буде робити — хз

Як я казав раніше, ви хочете все контролювати, всі аспекти. Це «сміття» ми не обробляємо, а використовуємо стандартні, закладені в мову механізми приведення типів. Тоді всі, хто не зміг довести, що транзакція пройшла, буде проігноровано. Якщо якась система неправильно проводить взаємодію з нашою системою, то це проблеми тієї системи.

1. Люди оперують абстракціями.
2. Моделювання і формалізація процесів має за мету створення спрощеної системи, достатньої для тих цілей, які нам потрібні.
Якщо нам потрібно вносити в модель і процеси взаємодії всередині зміни — значить, попередньо вона була недостатньо точно сконструйована, або змінилися наші вимоги до точності.
3. Процеси описують взаємодію об’єктів. Без визначення того, що взаємодіє (об’єктів) ви не можете описати процеси.
4. Транзакція на біржі не є реалізацією певних подій в «аналоговому світі». Ми маємо справу з абстракціями «продавець», «покупець», «кількість», «сума», «валюта». Від того що покупець є фізичною особою «дядя Вася з Житомира, у якого сьогодні похмілля» чи автоматизованою системою для гри на біржі, нічого не зміниться.
Так само варіанти розвитку подій після підписання договору нас цікавлять тільки деякі. Для нашої моделі цього достатньо. Для всього іншого в договорі записують пункти про розгляд спорів в суді і про форс-мажор.
5. Коли ви отримуєте «піщинку» на вхід, у вас є два варіанти: 1) не пропустити її далі, 2) пропустити її далі і спробувати «переварити»
Тут вибір, який варіант кращий, залежить, умовно кажучи, від впливу «піщинок». В тому числі кумулятивного — у адаптації є свої межі.
У JS слабка неявна типізація, тому у що перетвориться всякий мусор на вході всередині, іноді можна тільки здогадуватись. Але це, звичайно, не наші проблеми — «якщо якась система неправильно проводить взаємодію з нашою системою, то це проблеми тієї системи.»

1, 2. Дякую, Капітане!
3. Процеси можуть бути без взаємодії об’єктів. Наприклад зміна стану об’єкта з часом.
4. Транзакції на біржі — результат впровадження певного бізнес-процесу. Бізнес-процеси існують в аналоговому світі.
5. Самі оберіть варіант, якій більш стійкий до змін.

У JS слабка неявна типізація, тому у що перетвориться всякий мусор на вході всередині, іноді можна тільки здогадуватись.

Та не треба здогадуватися. В цьому немає сенсу. Візьмемо приклад. Керування автомобілем. Їм може керувати навіть щур. Та й щура не потрібно, достатньо його мозку. Так, прийдеться трохи додати прокладанок, щоб інтерфейс взаємодії був краще, але в цілому процес керування взагалі не має обмежень по вхідним даним.

мне кажется, автор местами путает понятия «Сильная и слабая» и «Статическая и динамическая». Например Python имеет Сильную(Строгую), но в то же время Динамическую

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

сначало должна быть языконезависимая платформа, а потом хоть на brainfuck-е пишите

Вместо костылей в существующем языке, лучше (проще и надежнее) использовать изначально типилированные языки, транслирующиеся в js. Проще всего взять ELM. Но есть еще PureScript с более развитой системой типов. А можно вообще взять Idris с полной поддержкой зависимых типов.

А есть еще Haxe, с отличной и очень даже чистой трансляцией в JS. С кучей фич а-ля pattern matching-а, макросов, генериков, рефлексии, возможностью миксовать код с JS и тд и тп.

Кстати, да, весьма приятный язык с незаслуженно низкой популярностью. У нас даже проект на нём есть.

Я вот думаю, статью может чиркануть... А то уж через чур мало людей о нем знают.

Это да, особенно кросскомпиляция.

Підписатись на коментарі