Como inicializo um object TypeScript com um object JSON

Eu recebo um object JSON de uma chamada AJAX para um servidor REST. Este object tem nomes de propriedades que correspondem à minha class TypeScript (este é um follow-on para esta questão ).

Qual é a melhor maneira de inicializá-lo? Eu não acho que isso funcionará porque a class (object JSON) tem membros que são listas de objects e membros que são classs, e essas classs possuem membros que são listas e / ou classs.

Mas eu prefiro uma abordagem que procure os nomes de membros e os designe, criando listas e instanciando classs conforme necessário, para que eu não tenha que escrever código explícito para cada membro em todas as classs (há MUITO!)

Estas são algumas fotos rápidas para mostrar algumas maneiras diferentes. Eles não são de forma alguma “completos” e como um aviso legal, eu não acho que seja uma boa idéia fazer assim. Além disso, o código não está muito limpo, já que acabei de digitar rapidamente.

Também como uma nota: É claro que as classs deserializáveis ​​precisam ter construtores padrão, como é o caso em todas as outras linguagens em que estou ciente da desserialização de qualquer tipo. É claro, o Javascript não vai reclamar se você chamar um construtor não-padrão sem argumentos, mas a class deve estar melhor preparada para isso (além disso, não seria a “maneira typescripty”).

Opção nº 1: Nenhuma informação de tempo de execução

O problema com essa abordagem é principalmente que o nome de qualquer membro deve corresponder à sua class. Qual limita automaticamente a um membro do mesmo tipo por class e quebra várias regras de boas práticas. Eu aconselho fortemente contra isso, mas apenas listá-lo aqui porque foi o primeiro “rascunho” quando eu escrevi esta resposta (que é também porque os nomes são “Foo” etc.).

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); 

Opção nº 2: a propriedade name

Para se livrar do problema na opção 1, precisamos ter algum tipo de informação sobre o tipo de um nó no object JSON. O problema é que, em Typescript, essas coisas são construções de tempo de compilation e precisamos delas em tempo de execução – mas os objects de tempo de execução simplesmente não têm consciência de suas propriedades até que sejam configurados.

Uma maneira de fazer isso é tornar as classs conscientes de seus nomes. Você precisa dessa propriedade no JSON também. Na verdade, você precisa disso no 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); 

Opção 3: declarar explicitamente os tipos de membros

Como dito acima, as informações de tipo dos membros da class não estão disponíveis em tempo de execução – isto é, a menos que as disponibilizemos. Nós só precisamos fazer isso para membros não-primitivos e estamos prontos:

 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); 

Opção # 4: A maneira detalhada, mas limpa

Atualização 01/03/2016: Como o @GameAlchemist apontou nos comentários, a partir do Typescript 1.7, a solução descrita abaixo pode ser escrita de uma maneira melhor usando decoradores de class / propriedade.

A serialização é sempre um problema e, na minha opinião, a melhor maneira é uma maneira que não é a mais curta. De todas as opções, é isso que prefiro porque o autor da class tem controle total sobre o estado dos objects desserializados. Se eu tivesse que adivinhar, eu diria que todas as outras opções, mais cedo ou mais tarde, vão colocar você em apuros (a menos que o Javascript surja com uma maneira nativa de lidar com isso).

Realmente, o exemplo a seguir não faz justiça à flexibilidade. Realmente copia apenas a estrutura da turma. A diferença que você deve ter em mente aqui, é que a class tem controle total para usar qualquer tipo de JSON que queira controlar o estado de toda a class (você pode calcular coisas, etc.).

 interface Serializable { deserialize(input: Object): T; } class Member implements Serializable { id: number; deserialize(input) { this.id = input.id; return this; } } class ExampleClass implements Serializable { 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); 

TLDR: TypedJSON (prova de conceito em funcionamento)


A raiz da complexidade desse problema é que precisamos desserializar JSON em tempo de execução usando informações de tipo que só existem em tempo de compilation . Isso requer que informações de tipo sejam disponibilizadas de alguma forma em tempo de execução.

Felizmente, isso pode ser resolvido de uma maneira muito elegante e robusta com decoradores e ReflectDecorators :

  1. Use decoradores de propriedade em propriedades que estão sujeitas a serialização, para registrar informações de metadados e armazenar essas informações em algum lugar, por exemplo, no protótipo de class
  2. Alimente essas informações de metadados para um inicializador recursivo (desserializador)

Gravando informações de tipo

Com uma combinação de ReflectDecorators e decoradores de propriedade, informações de tipo podem ser facilmente registradas sobre uma propriedade. Uma implementação rudimentar dessa abordagem seria:

 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); } 

Para qualquer propriedade, o snippet acima adicionará uma referência da function construtora da propriedade à propriedade __propertyTypes__ oculta no protótipo de class. Por exemplo:

 class Language { @JsonMember // String name: string; @JsonMember// Number level: number; } class Person { @JsonMember // String name: string; @JsonMember// Language language: Language; } 

E é isso, temos as informações de tipo necessárias em tempo de execução, que agora podem ser processadas.

Processando informações de tipo

Primeiro precisamos obter uma instância de Object usando JSON.parse – depois disso, podemos iterar sobre as inputs em __propertyTypes__ (coletadas acima) e instanciar as propriedades necessárias de acordo. O tipo do object raiz deve ser especificado, para que o desserializador tenha um ponto inicial.

Novamente, uma implementação simples dessa abordagem seria:

 function deserialize(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); 

A ideia acima tem uma grande vantagem de desserializar por tipos esperados (para valores complexos / objects), em vez do que está presente no JSON. Se uma Person for esperada, então é uma instância Person criada. Com algumas medidas de segurança adicionais em vigor para tipos primitivos e matrizes, essa abordagem pode ser segura, resistindo a qualquer JSON mal-intencionado.

Casos de borda

No entanto, se você está feliz que a solução é simples, eu tenho algumas más notícias: há um grande número de casos extremos que precisam ser resolvidos. Apenas alguns dos quais são:

  • Matrizes e elementos de matriz (especialmente em matrizes aninhadas)
  • Polimorfismo
  • Classes e interfaces abstratas

Se você não quer mexer com tudo isso (eu aposto que você não quer), eu ficaria feliz em recomendar uma versão experimental de uma prova de conceito utilizando essa abordagem, TypedJSON – que eu criei Para resolver esse problema exato, um problema eu me encaro diariamente.

Devido a como decoradores ainda estão sendo considerados experimental, eu não recomendaria usá-lo para uso em produção, mas até agora me serviu bem.

você pode usar Object.assign Eu não sei quando isso foi adicionado, eu estou usando atualmente o Typescript 2.0.2, e isso parece ser um recurso do ES6.

 client.fetch( '' ).then( response => { return response.json(); } ).then( json => { let hal : HalJson = Object.assign( new HalJson(), json ); log.debug( "json", hal ); 

aqui está HalJson

 export class HalJson { _links: HalLinks; } export class HalLinks implements Links { } export interface Links { readonly [text: string]: Link; } export interface Link { readonly href: URL; } 

aqui está o que cromo diz que é

 HalJson {_links: Object} _links : Object public : Object href : "http://localhost:9000/v0/public 

então você pode ver que não faz a atribuição recursivamente

Eu tenho usado esse cara para fazer o trabalho: https://github.com/weichx/cerialize

É muito simples, mas poderoso. Suporta:

  • Serialização e desserialização de uma tree inteira de objects.
  • Propriedades persistentes e transitórias no mesmo object.
  • Ganchos para personalizar a (de) lógica de serialização.
  • Pode (de) serializar em uma instância existente (excelente para Angular) ou gerar novas instâncias.
  • etc.

Exemplo:

 class Tree { @deserialize public species : string; @deserializeAs(Leaf) public leafs : Array; //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); 

Opção # 5: usando construtores datilografados e jQuery.extend

Este parece ser o método mais sustentável: adicione um construtor que tome como parâmetro a estrutura json e estenda o object json. Dessa forma, você pode analisar uma estrutura json em todo o modelo de aplicativo.

Não há necessidade de criar interfaces ou listar propriedades no construtor.

 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's property does not match the json's: this.city = jsonData.town; } } 

Em seu retorno de chamada ajax, onde você recebe uma empresa para calcular os salários:

 onReceiveCompany( jsonCompany : any ) { let newCompany = new Company( jsonCompany ); // call the methods on your newCompany object ... newCompany.calculateSalaries() } 

A quarta opção descrita acima é uma maneira simples e agradável de fazer isso, que tem que ser combinada com a segunda opção no caso em que você tem que lidar com uma hierarquia de classs como por exemplo uma lista de membros que é qualquer ocorrência de subclasss de uma superclass de membros, por exemplo, o diretor estende o membro ou o aluno estende o membro. Nesse caso você tem que dar o tipo de subclass no formato json

Eu criei uma ferramenta que gera interfaces TypeScript e um “mapa de tipo” em tempo de execução para executar a verificação de tipo de tempo de execução em relação aos resultados de JSON.parse : ts.quicktype.io

Por exemplo, dado este JSON:

 { "name": "David", "pets": [ { "name": "Smoochie", "species": "rhino" } ] } 

O quicktype produz a seguinte interface TypeScript e o tipo de mapa:

 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", }, }; 

Em seguida, verificamos o resultado de JSON.parse no mapa de tipos:

 export function fromJson(json: string): Person { return cast(JSON.parse(json), object("Person")); } 

Eu deixei de fora algum código, mas você pode tentar quicktype para os detalhes.

Talvez não seja solução real, mas simples:

 interface Bar{ x:number; y?:string; } var baz:Bar = JSON.parse(jsonString); alert(baz.y); 

trabalho para dependencies difíceis também !!!

JQuery .extend faz isso por você:

 var mytsobject = new mytsobject(); var newObj = {a:1,b:2}; $.extend(mytsobject, newObj); //mytsobject will now contain a & b 

Outra opção usando fábricas

 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

usar assim

 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 eg dates are Dates console.log('a.date is instanceof Date', a.date instanceof Date); console.log('ab is instanceof B', ab instanceof B); 
  1. mantém suas aulas simples
  2. injeção disponível para as fábricas para flexibilidade

você pode fazer como abaixo

 export interface Instance { id?:string; name?:string; type:string; } 

e

 var instance: Instance = ({ id: null, name: '', type: '' }); 
 **model.ts** export class Item { private key: JSON; constructor(jsonItem: any) { this.key = jsonItem; } } **service.ts** import { Item } from '../model/items'; export class ItemService { items: Item; constructor() { this.items = new Item({ 'logo': 'Logo', 'home': 'Home', 'about': 'About', 'contact': 'Contact', }); } getItems(): Item { return this.items; } }