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


Runtyper warning example


Что именно находим


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


1 === "1" // Strict compare of different types: 1 (number) === "1" (string)
[1, 2] + true  // Numeric operation with non-numeric value: "[1,2]" (object) + true (boolean)
42 * null  // Numeric operation with non-numeric value: 42 (number) * null
...

JavaScript молча "проглатывает" такие операции, хотя в большинстве случаев это опечатка, невнимательность или просто баг.


Как это работает


Под капотом Runtyper использует данные AST-дерева, предоставляемого бабелем. Плагин оборачивает операторы сравнения и арифметических действий в функцию, которая дополнительно проверяет типы аргументов.


Например, было:


if (x === y) { ... }

Стало (упрощенно):


if (strictEqual(x, y)) { ... }

function strictEqual(a, b) {
  if (typeof a !== typeof b) {
    console.warn('Strict compare of different types: ' + (typeof a) + ' === ' + (typeof b));
  }
  return a === b;
}

Про статический анализ


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


Вот пример из документации, где статический анализ не находит ошибку, но при выполнении кода Runtyper бросит предупреждение:


function square(n) {
  return n * n; // Numeric operation with non-numeric value: "Vasya" (string) * "Vasya" (string)
}

window.document.getElementById('username').addEventListener('change', function (event) {
  square(event.target.value);
});

Конечно, минусом проверки в runtime является то, что сама проверка происходит только при выполнении определенной строки кода. Если строка ни разу не выполнилась — то и проверки не будет. Поэтому самое правильное — включать плагин в development-сборку и в staging, на котором у вас гоняются тесты.


Но несомненный плюс в том, что не нужно писать аннотаций :) Просто подключаете плагин и получаете предупреждения об ошибках в консоль.


Подведем итог


Можно без особых усилий добавить в проект дополнительную проверку типов в runtime. Это позволит вам отловить еще какое-то количество багов до продакшена. Работает в браузере и в Node.js и не требует аннотаций кода. Документация и примеры использования есть на GitHub.

Поделиться с друзьями
-->

Комментарии (30)


  1. kovalevsky
    31.03.2017 09:52
    +13

    Проще взять TS, имхо


    1. Leopotam
      31.03.2017 09:58

      Или просто flow прикрутить как постпроцесс на сохранение файла — получится то же самое с теми же сообщениями об ошибках.


    1. Andre_487
      02.04.2017 16:37
      +1

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


  1. vintage
    31.03.2017 10:35

    Аннотации вам в любом случае писать придётся в документации. Так что лучше уж сразу писать формальные аннотации, по которым можно и документацию генерировать, чем лениться аннотировать код, а потом лениться обновлять документацию.


  1. impwx
    31.03.2017 11:17

    Какой-то ненадежный инструмент.

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

    С другой стороны, есть подозрение что он может сломать работающий код:

    function hasDuplicates(arr) {
        for(var i = 0; var i < arr.length - 1; i++) {
            for(var j = i + 1; j < arr.length; j++) {
                if(arr[i] === arr[j]) return true;
            }
        }
        return false;
    }
    
    var result = hasDuplicates(['a', true]); // выкинет ошибку?
    


    1. vitalets
      31.03.2017 11:38
      +2

      Сломать он ничего не сможет, потому что всего лишь выводит предупреждения в консоль, сохраняя результат операции.
      Для вашего примера — да, будет ворнинг, т. к. в одном массиве хранятся данные разных типов (что не есть хорошо):



      1. impwx
        31.03.2017 12:01
        -2

        Оператор === специально сделан для того, чтобы безопасно сравнивать объекты потенциально разных типов. Если есть гарантия, что сравниваемые объекты всегда одного типа — можно и == использовать.

        Если вы действительно проверяете с помощью простого typeof, как в примере, то у вас наверняка выдается ошибка при сравнении с null или undefined — хотя такое сравнение вполне корректно.

        Кстати говоря, массив из элементов разных типов — это нормальная практика в JS-библиотеках. Например, в Angular так указываются аннотации для Dependency Injection.


        1. vitalets
          31.03.2017 13:03

          Если есть гарантия, что сравниваемые объекты всегда одного типа — можно и == использовать.

          С точки зрения синтаксиса да, но с точки зрение надежности, я бы не рекомендовал использовать "==".


          Если вы действительно проверяете с помощью простого typeof, как в примере, то у вас наверняка выдается ошибка при сравнении с null или undefined — хотя такое сравнение вполне корректно.

          Про явное сравнение x === null и x === undefined абсолютно согласен, в таких случаях разработчик явно показывает, что ожидает в переменной null|udnefined, поэтому предупреждений не будет.


    1. iShatokhin
      31.03.2017 15:32

      Можно побыть немного занудой? Если в массиве будет NaN, ваш код сам сломается. Лучше так:


      function hasDuplicates(arr) {
          for(var i = 0; i < arr.length - 1; i++) {
              if(arr.includes(arr[i], i + 1)) return true;
          }
          return false;
      }
      
      var result = hasDuplicates([NaN, 'a', true, NaN]); // ваша версия вернет false, моя - true


      1. impwx
        31.03.2017 15:49

        Это спорный момент. Я бы не ожидал, что hasDuplicates считает NaN равным самому себе, поскольку обычный оператор сравнения так не делает. Но всё зависит от контекста.


  1. ArmorDarks
    31.03.2017 12:35
    +2

    Есть еще подобный плагин для проверки в рантайме, но для Flow: babel-plugin-tcomb. Сам tcomb тоже стоит внимания.


    1. vitalets
      31.03.2017 13:07

      Спасибо! Tcomb смотрел, а вот плагин не видел.


      1. zorro1211
        31.03.2017 18:02
        +1

        Ещё есть http://objectmodel.js.org/, но без babel плагина


  1. Aquahawk
    31.03.2017 16:23

    это работает только с примитивными типами? Оно же даже массив с объектом отличить не сможет? И это же жесточайший удар по производительности. Нужна типизация? Смотрите на TypeScript или FlowType.


    1. vitalets
      31.03.2017 17:56

      это работает только с примитивными типами?

      пока да.


      Оно же даже массив с объектом отличить не сможет?

      на это есть issue


      И это же жесточайший удар по производительности

      это догадка или у вас есть пример с цифрами? На моих сборках разницы нет.


      1. Aquahawk
        31.03.2017 18:16

        Бремя доказательства лежит на утверждающем.
        Про производительность хотел бы посмотреть тесты и методику их постановки. Не то чтобы я не мог их поставить сам, но я не думаю что мне это нужно, всё и так понятно, не бывает бесплатного исполнения дополнительного кода.


        1. Murmurianez
          01.04.2017 15:22
          +2

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


    1. Semigradsky
      01.04.2017 21:05
      +2

      Ну какой удар по производительности? Не будите же вы это в продакшене использовать. Это для этапа разработки.


      1. vitalets
        01.04.2017 21:19
        +1

        Да, именно.


        Кстати по сравнению с тем, что происходит например при транспайлинге es6 --> es5, то тут оверхед совсем небольшой: мы же и так в коде иногда пишем явную проверку через typeof в условиях.


        1. Aquahawk
          02.04.2017 13:45

          что происходит например при транспайлинге es6 --> es5

          а что там происходит? Можно конкретные примеры? Опять же с замерами если можно.


          1. vitalets
            02.04.2017 15:53

            Пример можно взять прямо из REPL на сайте бабеля: https://babeljs.io/repl/
            Было:


            class A extends B {}

            Стало:


            "use strict";
            
            function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
            
            function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
            
            function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
            
            var A = function (_B) {
              _inherits(A, _B);
            
              function A() {
                _classCallCheck(this, A);
            
                return _possibleConstructorReturn(this, (A.__proto__ || Object.getPrototypeOf(A)).apply(this, arguments));
              }
            
              return A;
            }(B);


            1. Aquahawk
              02.04.2017 16:00

              это я видел. А тесты производительности. Вы их ставили?


              1. vitalets
                05.04.2017 12:36

                Да: https://jsperf.com/runtyper-equal-perf


                Сравним насколько медленнее становится === если заменить его на == и на самовызывающийся strictEqual(x, y) с проверкой типов:


                Chrome 57:
                == — 67% slower
                strictEqual() — 94% slower


                YaBrowser 17.3:
                == — 84% slower
                strictEqual() — 94% slower


                Firefox 52:
                == — 99% slower
                strictEqual() — same time(!)


                1. Aquahawk
                  05.04.2017 14:19
                  +2

                  такой тест в корне невалиден. Вот тут как раз мой доклад про бенчмарки доступен, там частично раскрыта тема того почему так бенчмаркать нельзя https://habrahabr.ru/company/superjob/blog/325512/


                  1. vitalets
                    05.04.2017 15:30

                    ок, посмотрю.


                  1. vitalets
                    05.04.2017 16:05

                    Да, полезный доклад.
                    Хотя в данном случае после заворачивания в функции получились ровно такие же результаты.


                    1. Aquahawk
                      05.04.2017 16:11

                      Такие мелкие штуки нельзя так тестить и в функцию заворачивая, там скорее всего вообще всё будет выброшено, нужно уже подробно смотреть что там наджитилось, а лучше бенчмаркать реальную задачу которая занимает какое-то ненулевое время, чтобы она долго выполнялась.


                      1. vitalets
                        05.04.2017 16:46

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

                        Подскажешь, как правильно написать тест в данной ситуации?


                        1. Aquahawk
                          05.04.2017 17:57
                          +1

                          Не обещаю на неделе, но может к выходным и напишу.


      1. justfly1984
        02.04.2017 11:23
        +1

        Иногда производительность важна и в NODE_ENV=development. У меня highload realtime webapp, в деве постоянно пухнет от утечек памяти и consol.log. А в NODE_ENV=production занимает максимум 32Mb в тестах на сутки и более.