Я получаю объект JSON от вызова AJAX на сервер REST. Этот объект имеет имена свойств, которые соответствуют моему классу TypeScript (это продолжение этого вопроса).
Каков наилучший способ его инициализации? Я не думаю, что this будет работать, потому что класс (& JSON object) имеет членов, которые представляют собой списки объектов и членов, которые являются классами, и эти классы имеют членов, которые являются списками и/или классами.
Но я бы предпочел подход, который ищет имена участников и назначает их, создавая списки и создавая классы по мере необходимости, поэтому мне не нужно писать явный код для каждого члена в каждом классе (там LOT! )
Вот несколько быстрых снимков, чтобы показать несколько разных способов. Они ни в коем случае не "полны" и как отказ от ответственности, я не думаю, что это хорошая идея сделать это так. Также код не слишком чист, так как я просто набрал его вместе довольно быстро.
Также как примечание: Конечно, десериализуемые классы должны иметь конструкторы по умолчанию, как это имеет место на всех других языках, где я знаю о десериализации любого рода. Конечно, Javascript не будет жаловаться, если вы вызываете конструктор не по умолчанию без аргументов, но класс лучше подготовиться к нему (плюс, на самом деле это не будет "способ создания скриптов" ).
Проблема с этим подходом в основном состоит в том, что имя любого члена должно соответствовать его классу. Это автоматически ограничивает вас одним членом одного типа для каждого класса и нарушает несколько правил хорошей практики. Я категорически против этого, но просто перечислил его здесь, потому что это был первый "проект", когда я написал этот ответ (что также объясняет имена "Foo" и т.д.).
module Environment {
export class Sub {
id: number;
}
export class Foo {
baz: number;
Sub: Sub;
}
}
function deserialize(json, environment, clazz) {
var instance = new clazz();
for(var prop in json) {
if(!json.hasOwnProperty(prop)) {
continue;
}
if(typeof json[prop] === 'object') {
instance[prop] = deserialize(json[prop], environment, environment[prop]);
} else {
instance[prop] = json[prop];
}
}
return instance;
}
var json = {
baz: 42,
Sub: {
id: 1337
}
};
var instance = deserialize(json, Environment, Environment.Foo);
console.log(instance);
Чтобы избавиться от проблемы в опции №1, нам нужно иметь некоторую информацию о том, какой тип a node в объекте JSON. Проблема в том, что в Typescript эти вещи являются конструкциями времени компиляции, и мы нуждаемся в них во время выполнения, но объекты времени выполнения просто не знают о своих свойствах до тех пор, пока они не будут установлены.
Один из способов сделать это - сделать классы осведомленными о своих именах. Вам также нужно это свойство в JSON. На самом деле, вам нужно только это в json:
module Environment {
export class Member {
private __name__ = "Member";
id: number;
}
export class ExampleClass {
private __name__ = "ExampleClass";
mainId: number;
firstMember: Member;
secondMember: Member;
}
}
function deserialize(json, environment) {
var instance = new environment[json.__name__]();
for(var prop in json) {
if(!json.hasOwnProperty(prop)) {
continue;
}
if(typeof json[prop] === 'object') {
instance[prop] = deserialize(json[prop], environment);
} else {
instance[prop] = json[prop];
}
}
return instance;
}
var json = {
__name__: "ExampleClass",
mainId: 42,
firstMember: {
__name__: "Member",
id: 1337
},
secondMember: {
__name__: "Member",
id: -1
}
};
var instance = deserialize(json, Environment);
console.log(instance);
Как указано выше, информация о типе членов класса недоступна во время выполнения - то есть, если мы не сделаем ее доступной. Нам нужно только сделать это для не примитивных членов, и мы будем рады:
interface Deserializable {
getTypes(): Object;
}
class Member implements Deserializable {
id: number;
getTypes() {
// since the only member, id, is primitive, we don't need to
// return anything here
return {};
}
}
class ExampleClass implements Deserializable {
mainId: number;
firstMember: Member;
secondMember: Member;
getTypes() {
return {
// this is the duplication so that we have
// run-time type information :/
firstMember: Member,
secondMember: Member
};
}
}
function deserialize(json, clazz) {
var instance = new clazz(),
types = instance.getTypes();
for(var prop in json) {
if(!json.hasOwnProperty(prop)) {
continue;
}
if(typeof json[prop] === 'object') {
instance[prop] = deserialize(json[prop], types[prop]);
} else {
instance[prop] = json[prop];
}
}
return instance;
}
var json = {
mainId: 42,
firstMember: {
id: 1337
},
secondMember: {
id: -1
}
};
var instance = deserialize(json, ExampleClass);
console.log(instance);
Обновление 01/03/2016: Как отметил @GameAlchemist в комментариях, с Typescript 1.7, решение, описанное ниже, может быть написано лучше с использованием декораторов класса/свойств.
Сериализация всегда является проблемой, и, на мой взгляд, лучший способ - это путь, который не является самым коротким. Из всех вариантов это то, что я бы предпочел, потому что автор класса имеет полный контроль над состоянием десериализованных объектов. Если бы я должен был догадаться, я бы сказал, что все другие варианты, рано или поздно, вызовут вас в неприятности (если Javascript не найдет родной способ справиться с этим).
Действительно, следующий пример не оправдывает гибкость. Это действительно просто копирует структуру класса. Разница, которую вы должны иметь в виду здесь, заключается в том, что класс имеет полный контроль над использованием любого типа JSON, который хочет контролировать состояние всего класса (вы могли бы рассчитать и т.д.).
interface Serializable<T> {
deserialize(input: Object): T;
}
class Member implements Serializable<Member> {
id: number;
deserialize(input) {
this.id = input.id;
return this;
}
}
class ExampleClass implements Serializable<ExampleClass> {
mainId: number;
firstMember: Member;
secondMember: Member;
deserialize(input) {
this.mainId = input.mainId;
this.firstMember = new Member().deserialize(input.firstMember);
this.secondMember = new Member().deserialize(input.secondMember);
return this;
}
}
var json = {
mainId: 42,
firstMember: {
id: 1337
},
secondMember: {
id: -1
}
};
var instance = new ExampleClass().deserialize(json);
console.log(instance);
equals
или toString
(только если они обычно генерируются автоматически). При желании не должно быть слишком сложно написать генератор для deserialize
, но это не может быть автоматизация во время выполнения.
TL;DR: TypedJSON (рабочее доказательство концепции)
Корень сложности этой проблемы заключается в том, что нам нужно десериализовать JSON во время выполнения, используя информацию о типе, которая существует только во время компиляции. Это требует, чтобы информация о типе каким-то образом была доступна во время выполнения.
К счастью, это можно решить очень элегантным и надежным способом с помощью decorators и ReflectDecorators:
С помощью комбинации ReflectDecorators и декораторов свойств информацию о типе можно легко записать о свойстве. Рудиментарная реализация этого подхода будет заключаться в следующем:
function JsonMember(target: any, propertyKey: string) {
var metadataFieldKey = "__propertyTypes__";
// Get the already recorded type-information from target, or create
// empty object if this is the first property.
var propertyTypes = target[metadataFieldKey] || (target[metadataFieldKey] = {});
// Get the constructor reference of the current property.
// This is provided by TypeScript, built-in (make sure to enable emit
// decorator metadata).
propertyTypes[propertyKey] = Reflect.getMetadata("design:type", target, propertyKey);
}
Для любого заданного свойства вышеприведенный фрагмент добавит ссылку функции конструктора свойства в скрытое свойство __propertyTypes__
в прототипе класса. Например:
class Language {
@JsonMember // String
name: string;
@JsonMember// Number
level: number;
}
class Person {
@JsonMember // String
name: string;
@JsonMember// Language
language: Language;
}
И что он, у нас есть требуемая информация о типе во время выполнения, которая теперь может быть обработана.
Сначала нам нужно получить экземпляр Object
, используя JSON.parse
- после этого мы можем выполнить итерацию по entires в __propertyTypes__
(собрано выше) и соответствующим образом создать необходимые свойства. Тип корневого объекта должен быть указан, так что десериализатор имеет начальную точку.
Опять же, простая простая реализация этого подхода будет заключаться в следующем:
function deserialize<T>(jsonObject: any, Constructor: { new (): T }): T {
if (!Constructor || !Constructor.prototype.__propertyTypes__ || !jsonObject || typeof jsonObject !== "object") {
// No root-type with usable type-information is available.
return jsonObject;
}
// Create an instance of root-type.
var instance: any = new Constructor();
// For each property marked with @JsonMember, do...
Object.keys(Constructor.prototype.__propertyTypes__).forEach(propertyKey => {
var PropertyType = Constructor.prototype.__propertyTypes__[propertyKey];
// Deserialize recursively, treat property type as root-type.
instance[propertyKey] = deserialize(jsonObject[propertyKey], PropertyType);
});
return instance;
}
var json = '{ "name": "John Doe", "language": { "name": "en", "level": 5 } }';
var person: Person = deserialize(JSON.parse(json), Person);
Вышеупомянутая идея имеет большое преимущество десериализации по ожидаемым типам (для сложных/объектных значений) вместо того, что присутствует в JSON. Если ожидается Person
, то создается экземпляр Person
. С некоторыми дополнительными мерами безопасности для примитивных типов и массивов этот подход можно сделать безопасным, что противостоит любому вредоносному JSON.
Однако, если вы теперь счастливы, что решение настолько простое, у меня есть плохие новости: есть огромное количество крайних случаев, о которых нужно позаботиться. Только некоторые из них:
Если вы не хотите возиться со всеми этими (я уверен, вы этого не сделаете), я был бы рад рекомендовать рабочую экспериментальную версию доказательной концепции, использующей этот подход, TypedJSON - который я создал для решения этой точной проблемы, проблемы, с которой я сталкиваюсь ежедневно.
Из-за того, что декораторы все еще считаются экспериментальными, я бы не рекомендовал использовать его для использования в производстве, но до сих пор он хорошо меня обслуживал.
вы можете использовать Object.assign
Я не знаю, когда это было добавлено, я в настоящее время использую Typescript 2.0.2, и это похоже на функцию ES6.
client.fetch( '' ).then( response => {
return response.json();
} ).then( json => {
let hal : HalJson = Object.assign( new HalJson(), json );
log.debug( "json", hal );
здесь HalJson
export class HalJson {
_links: HalLinks;
}
export class HalLinks implements Links {
}
export interface Links {
readonly [text: string]: Link;
}
export interface Link {
readonly href: URL;
}
здесь, что хром говорит, что это
HalJson {_links: Object}
_links
:
Object
public
:
Object
href
:
"http://localhost:9000/v0/public
чтобы вы могли видеть, что это не рекурсивно присваивается
Object.assign
. Почему тогда у нас есть два лексиконоподобных ответа над этим?
Object.assign
не будет работать рекурсивно и не будет создавать экземпляры правильных типов объектов, оставляя значения как экземпляры Object
. Хотя это нормально для тривиальных задач, сериализация сложных типов невозможна. Например, если свойство класса имеет пользовательский тип класса, JSON.parse
+ Object.assign
создаст экземпляр этого свойства для Object
. Побочные эффекты включают отсутствующие методы и средства доступа.
Я использовал этого парня, чтобы выполнить эту работу: https://github.com/weichx/cerialize
Это очень простой, но мощный. Он поддерживает:
Пример:
class Tree {
@deserialize public species : string;
@deserializeAs(Leaf) public leafs : Array<Leaf>; //arrays do not need extra specifications, just a type.
@deserializeAs(Bark, 'barkType') public bark : Bark; //using custom type and custom key name
@deserializeIndexable(Leaf) public leafMap : {[idx : string] : Leaf}; //use an object as a map
}
class Leaf {
@deserialize public color : string;
@deserialize public blooming : boolean;
@deserializeAs(Date) public bloomedAt : Date;
}
class Bark {
@deserialize roughness : number;
}
var json = {
species: 'Oak',
barkType: { roughness: 1 },
leafs: [ {color: 'red', blooming: false, bloomedAt: 'Mon Dec 07 2015 11:48:20 GMT-0500 (EST)' } ],
leafMap: { type1: { some leaf data }, type2: { some leaf data } }
}
var tree: Tree = Deserialize(json, Tree);
Это, по-видимому, самый поддерживаемый метод: добавьте конструктор, который принимает в качестве параметра структуру json и расширяет объект json. Таким образом, вы можете проанализировать структуру json во всей модели приложения.
Нет необходимости создавать интерфейсы или перечислять свойства в конструкторе.
export class Company
{
Employees : Employee[];
constructor( jsonData: any )
{
jQuery.extend( this, jsonData);
// apply the same principle to linked objects:
if ( jsonData.Employees )
this.Employees = jQuery.map( jsonData.Employees , (emp) => {
return new Employee ( emp ); });
}
calculateSalaries() : void { .... }
}
export class Employee
{
name: string;
salary: number;
city: string;
constructor( jsonData: any )
{
jQuery.extend( this, jsonData);
// case where your object property does not match the json's:
this.city = jsonData.town;
}
}
В вашем обратном вызове ajax, где вы получаете компанию для расчета заработной платы:
onReceiveCompany( jsonCompany : any )
{
let newCompany = new Company( jsonCompany );
// call the methods on your newCompany object ...
newCompany.calculateSalaries()
}
$.extend
?
Возможно, это не фактическое, а простое решение:
interface Bar{
x:number;
y?:string;
}
var baz:Bar = JSON.parse(jsonString);
alert(baz.y);
работать и для сложных зависимостей!!!
baz
будет иметь тип Object
а не тип Bar.
Это работает в этом простом случае, потому что у Bar
нет методов (только примитивные свойства). Если бы у Bar
был метод, подобный isEnabled()
, этот подход потерпел бы неудачу, поскольку этот метод не был бы в сериализованной строке JSON.
4-й вариант, описанный выше, - это простой и приятный способ сделать это, который должен быть объединен со вторым вариантом в случае, когда вам нужно обрабатывать иерархию классов, например, список членов, который является любым из случаев подклассов суперкласса члена, например, директор продлевает член или учащийся. В этом случае вы должны указать тип подкласса в формате json
Я создал инструмент, который генерирует интерфейсы TypeScript и карту типа времени выполнения для выполнения проверки типа времени выполнения по результатам JSON.parse
: ts.quicktype.io
Например, учитывая этот JSON:
{
"name": "David",
"pets": [
{
"name": "Smoochie",
"species": "rhino"
}
]
}
quicktype создает следующий интерфейс TypeScript и тип карты:
export interface Person {
name: string;
pets: Pet[];
}
export interface Pet {
name: string;
species: string;
}
const typeMap: any = {
Person: {
name: "string",
pets: array(object("Pet")),
},
Pet: {
name: "string",
species: "string",
},
};
Затем мы проверяем результат JSON.parse
на карту типа:
export function fromJson(json: string): Person {
return cast(JSON.parse(json), object("Person"));
}
Я забыл какой-то код, но вы можете попробовать quicktype для деталей.
Другая опция, использующая заводы
export class A {
id: number;
date: Date;
bId: number;
readonly b: B;
}
export class B {
id: number;
}
export class AFactory {
constructor(
private readonly createB: BFactory
) { }
create(data: any): A {
const createB = this.createB.create;
return Object.assign(new A(),
data,
{
get b(): B {
return createB({ id: data.bId });
},
date: new Date(data.date)
});
}
}
export class BFactory {
create(data: any): B {
return Object.assign(new B(), data);
}
}
https://github.com/MrAntix/ts-deserialize
используйте
import { A, B, AFactory, BFactory } from "./deserialize";
// create a factory, simplified by DI
const aFactory = new AFactory(new BFactory());
// get an anon js object like you'd get from the http call
const data = { bId: 1, date: '2017-1-1' };
// create a real model from the anon js object
const a = aFactory.create(data);
// confirm instances e.g. dates are Dates
console.log('a.date is instanceof Date', a.date instanceof Date);
console.log('a.b is instanceof B', a.b instanceof B);
JQuery.extend делает это для вас:
var mytsobject = new mytsobject();
var newObj = {a:1,b:2};
$.extend(mytsobject, newObj); //mytsobject will now contain a & b
вы можете сделать, как показано ниже
export interface Instance {
id?:string;
name?:string;
type:string;
}
и
var instance: Instance = <Instance>({
id: null,
name: '',
type: ''
});