Я очень люблю статические типы, поэтому TypeScript стал незаменимым помощником при работе с NodeJS или браузерным JS.
По долгу службы приходится иметь очень много дел с JSON, и здесь система типов TypeScript не помогает ничем, даже мешает, ведь компилятор сообщает об отсутствии ошибок, JSON.parse возвращает тип Any. Кроме того, TypeScript не поддерживает рефлексию, ввиду специфики работы, а значит, нет возможности проверить тип, основываясь на уже существующем коде. Также, до последнего времени, средств для мета-программирования не было вовсе.
Зачастую проверка корректности пришедшего JSON-объекта оборачивается громадным кодом в конструкторах классов, либо такими же конфигурационными файлами. Но, наконец-то, в TypeScript 1.5 появились декораторы.
Декораторы позволяют выполнить некие манипуляции с классом, методом, свойством или параметром во время их объявления, при этом возможна передача дополнительной информации о декорируемом объекте. Этим я и воспользовался.
Выглядит это так:
Получилось небольшое дублирование при указании типа, но это гораздо меньшее зло, чем описывать систему классов отдельно, чисто для проверки типов.
Нам необходимо получить из JSON-объекта полноценный TypeScript-объект без ошибок в полях, с иерархической поддержкой вложенных объектов (со своими типами). Я не реализовывал работу с JSON, в корне которого не plain-object; не было необходимости.
Основные типы, которые я хотел получить на выходе из JSON:
Для реализации я создал декоратор свойства и специальный класс Model, который должны наследовать все классы, поля которых мы хотим проверять. При определении класса, в специальное поле заносится информация обо всех полях, объявленных с декоратором (тип, обязательное ли и т.д.). Кстати, можно не использовать наследование, а создать декоратор класса (например, @jsonable).
String — просто проверяется на собственно JS-объект String. Number и Boolean имеют возможность указать параметр isCasting, в таком случае проверяется, может ли значение быть приведенным к числу или возможно любое значение для Boolean.
Object — проверяется, является ли объект plain-object и если задан специальный параметр class, то создается его экземпляр, при этом если класс наследует Model, то, соответственно, проверятся и его поля.
Array — проверка на JS-Array + возможность указать тип перечисляемых значений. Каждое значение проверяется на тип отдельно.
Enum — если задан параметр class (в данном случае ссылка на enum), то проверяется, существует ли в этом перечислении нужное значение. В JSON задается текстом (как в определении enum).
Код здесь я приводить не буду, он достаточно простой, выложил его на github. Вот пример использования для классов, описанных листингом выше:
Как я уже сказал, это пока только прототип, в боевом режиме не участвовал, однако он дает почву для аналогичных решений или возможности использования декораторов.
P.S. Недавно на Хабре была опубликована статья о простоте Go — «Сложно о простоте Go». Так вот, новый ключевой символ @ в TypeScript уже позволил мне уменьшить количество кода в разы, а запомнил я его сразу же. А как я радовался появлению arrow-function! С нетерпением жду async/await (да, да, 2 новых слова), которые позволят избавиться еще от тонны then, when, resolve, reject и т.д.
По долгу службы приходится иметь очень много дел с 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)
some_x
09.07.2015 10:22+2Здорово! По собственному опыту подтверждаю что это очень востребованное решение.
vintage
Лучше так: