Проверка JSON при помощи декораторов в TypeScript

Я очень люблю статические типы, поэтому TypeScript стал незаменимым помощником при работе с NodeJS или браузерным JS.

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

Зачастую проверка корректности пришедшего JSON-объекта оборачивается громадным кодом в конструкторах классов, либо такими же конфигурационными файлами. Но, наконец-то, в TypeScript 1.5 появились декораторы.

Декораторы позволяют выполнить некие манипуляции с классом, методом, свойством или параметром во время их объявления, при этом возможна передача дополнительной информации о декорируемом объекте. Этим я и воспользовался.

Выглядит это так:

class X extends Model {
    @prop({ type: PropType.Array, arrayProp: { type: PropType.Number } })
    a;
}
enum Enum1 {
    V1,
    V2,
    V3
}
class MyClass extends Model {
    @prop({ type: PropType.Object, class: X })
    prop1;
    @prop({ type: PropType.String })
    propString;
    @prop({ type: PropType.Enum, class: Enum1 })
    b: Enum1;
}
class TestString extends Model {
    @prop({ type: PropType.String })
    prop: string;
}

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

Задача


Нам необходимо получить из JSON-объекта полноценный TypeScript-объект без ошибок в полях, с иерархической поддержкой вложенных объектов (со своими типами). Я не реализовывал работу с JSON, в корне которого не plain-object; не было необходимости.

Основные типы, которые я хотел получить на выходе из JSON:
  • String
  • Number
  • Boolean
  • Object
  • Array
  • Enum
  • Any

Решение


Для реализации я создал декоратор свойства и специальный класс Model, который должны наследовать все классы, поля которых мы хотим проверять. При определении класса, в специальное поле заносится информация обо всех полях, объявленных с декоратором (тип, обязательное ли и т.д.). Кстати, можно не использовать наследование, а создать декоратор класса (например, @jsonable).

String — просто проверяется на собственно JS-объект String. Number и Boolean имеют возможность указать параметр isCasting, в таком случае проверяется, может ли значение быть приведенным к числу или возможно любое значение для Boolean.

Object — проверяется, является ли объект plain-object и если задан специальный параметр class, то создается его экземпляр, при этом если класс наследует Model, то, соответственно, проверятся и его поля.
Array — проверка на JS-Array + возможность указать тип перечисляемых значений. Каждое значение проверяется на тип отдельно.
Enum — если задан параметр class (в данном случае ссылка на enum), то проверяется, существует ли в этом перечислении нужное значение. В JSON задается текстом (как в определении enum).

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

var a = new MyClass({ propString: "test1", prop1: { a: [1, 2, 3] }, b: "V1" });
console.assert(a.prop1 instanceof X);
console.assert(a.b === Enum1.V1, "Invalid JSON");
console.assert(a.propString === "test1");
try {
    new TestString({ prop: 123 });
    console.assert(false, "Not check string field");
} catch (e) {
}
try {
    new MyClass({});
    console.assert(false, "Not check required field");
} catch (e) {

}

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

P.S. Недавно на Хабре была опубликована статья о простоте Go — «Сложно о простоте Go». Так вот, новый ключевой символ @ в TypeScript уже позволил мне уменьшить количество кода в разы, а запомнил я его сразу же. А как я радовался появлению arrow-function! С нетерпением жду async/await (да, да, 2 новых слова), которые позволят избавиться еще от тонны then, when, resolve, reject и т.д.
Поделиться публикацией

Комментарии 2

    +1
    Лучше так:
    class MyClass extends Model {
        @PropType.Object( X )
        prop1;
        @PropType.String()
        propString;
        @PropType.Enum( Enum1 )
        b: Enum1;
    }
    
      +2
      Здорово! По собственному опыту подтверждаю что это очень востребованное решение.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое