How can you compare $Shape in flow on a deep nested object? - reactjs

The issue I have is that I want to create a function that takes an object that is a small part of a larger object and deep merges it. However, I want it strongly typed to catch if/when the 'big' object's shape changes.
I am including a contrived example below.
/* #flow */
type ContrivedType = {
nested: {
name: string,
optionalValue1?: string,
optionalValue2?: string,
}
}
let contrived: ContrivedType = {
nested: {name: "Required Value"}
}
type UpdateObject = $Shape<ContrivedType>
const update = (updateObject: UpdateObject): void => {
contrived = {
...contrived,
...updateObject,
nested: {
...contrived?.nested,
...updateObject?.nested
}
}
}
update({nested: {name: "should work", optionalValue1: "should work"}})
update({nested: {optionalValue1: "won't work"}})
Here you can see the results
Cannot call 'update' with object literal bound to 'updateObject' because property 'name' is missing in object literal [1] but exists in object type [2] in property 'nested'. [prop-missing]
How can I make this work?

You can create your own $DeepShape type as seen in use here
The code I used is
type $DeepShape<O: Object> = Object & $Shape<$ObjMap<O, (<V: Object>(V) => $DeepShape<V>) | (<V>(V) => V)>>
and is from this Github thread
Here is the final result of the above code:
/* #flow */
type ContrivedType = {
nested: {
name: string,
optionalValue1?: string,
optionalValue2?: string,
}
}
let contrived: ContrivedType = {
nested: {name: "Required Value"}
}
type $DeepShape<O: Object> = Object & $Shape<$ObjMap<O, (<V: Object>(V) => $DeepShape<V>) | (<V>(V) => V)>>
type UpdateObject = $DeepShape<ContrivedType>
const update = (updateObject: UpdateObject): void => {
contrived = {
...contrived,
...updateObject,
nested: {
...contrived?.nested,
...updateObject?.nested
}
}
}
update({nested: {name: "should work", optionalValue1: "should work"}})
update({nested: {optionalValue1: "won't work"}})
Would love any feedback on any issues or concerns people may have.

Related

How to properly infer type in typescript

I'm pretty new to typescript, this was probably answered before, but I can't find the answer anywhere, so here is the problem I'm trying to solve.
I have an enum
enum TableNames = {
example: 'example'
example2: 'example2',
}
and I have these types for my models
type TableName = keyof typeof TableNames
type TableState<L =any> = {
list: Array<L>
}
type ExampleDataType = {
id: string
...
}
type ExampleData2Type = {
id: number
...
}
type TableStateType = {
example: TableState<ExampleDataType>
example2: TableState<ExampleData2Type>
}
type InterListType<TName extends TableName> = TableStateType[TName]['list'][number]
and I have this special function
const createPath = <TName extends TableNames, T = InferListType<TName>>(
tableName: TName,
record: T
) => {
if (tableName === TableNames.example) {
console.log(record) // how to get this to infer to "ExampleDataType"
}
if (tableName === TableNames.example2) {
console.log(record) // how to get this to infer to "ExampleData2Type"
}
}
The question is the comment above, right now I'm getting this
if (tableName === TableNames.example) {
console.log(record) // T = InferListType<TName>
}
NOTE: typescript version is 3.7.4
You are running into this issue: Typescript - How do I narrow type possibilities of a generic type in a switch statement?.
Here's why [it] won't work: Typescript has control-flow type narrowing for variables like tableName, but not for type parameters like T.
JCalz mentions a workaround in the comments that might help. Looks something like this:
type ExampleDataType = {
id: string
data1: string
}
type ExampleDataType2 = {
id: string
data2: string
}
type TableToRecordMap = {
example: ExampleDataType
example2: ExampleDataType2
}
type TableName = keyof TableToRecordMap
type TableState<L =any> = {
list: Array<L>
}
type TableStateType = {
[Table in TableName]: TableState<TableToRecordMap[Table]>
}
const createPath = <TName extends TableName>(
tableName: TName,
record: TableToRecordMap[TName]
) => {
const partialTable: Partial<TableToRecordMap> = { [tableName]: record };
if (partialTable.example) {
console.log(partialTable.example)
} else if (partialTable.example2) {
console.log(partialTable.example2)
}
};
Playground link

Easiest way to create a Hash of Arrays (or equivalent) in Typescript?

I am looking to create a Hash of Arrays (or some equivalent structure) that allows me to collect an unknown set of properties (keyed by name) and have each property store an array of things that claimed they have said property.
const currentProperties = currentObject.getProperties();
// we can assume getProperties correctly returns an array of valid properties
currentProperties.forEach( (v) => {
  HoA[ v ].push( currentObject );
});
I want to be able to do something like the above to populate the Hash of Arrays - but how to I actually properly initialize it/do all of the TypeScript stuff? Currently I've been getting by using an enum to manually specify the possible properties that could show up, but I want to adapt it out to a structure that doesn't need to have a property list ahead of time, and can just take whatever shows up as a key.
As noted above, I understand how to solve a version of this problem if I manually specify the expected types of properties to be seen and use a bunch of
if (currentProperties.includes(Properties.exampleOne)) {
this.exampleGroupOne.push(currentObject);
}
but I want to be able to have this work with no prior knowledge of what values of properties exist.
EDIT: some clarification on what I am asking for -
The goal is to have a bunch of objects that have a getProperties() method that returns an array of zero or more attributes. I want to have a data structure that, for each attribute that exists, ends up with an array of the objects that reported that attribute. That is easy when I know the possible attributes ahead of time, but in this case, I won't. For actually acting on the attributes, I'll need a loop that is the attributes on the outer layer [the hash] and the relevant objects on the inner layer [the array]. (This is why I'm assuming HoA)
EDIT #2:
class Alice {
myProps(): string[] {
return ["Apple"];
}
}
class Bob {
myProps(): string[] {
return ["Banana"];
}
}
class Charlie {
myProps(): string[] {
return ["Apple", "Banana"];
}
}
const FruitBasket:{ [prop: string]: string} = {}
const myAlice = new Alice();
const myBob = new Bob();
const myCharlie = new Charlie();
const Objects = [myAlice, myBob, myCharlie];
for (const currentObject of Objects) {
const fruits = currentObject.myProps();
fruits.forEach( (v) => { FruitBasket[v].push(currentObject);});
}
I think this is almost what I want - I am getting an error that push does not exist on type string, but at this point I think I'm just missing something basic because I've been staring at this too long.
EDIT #3:
abstract class JustSomeGuy {
myProps(): string[] {
return [];
}
myName(): string {
return '';
}
}
class Alice extends JustSomeGuy {
myProps(): string[] {
return ["Apple"];
}
myName(): string {
return 'Alice';
}
}
class Bob extends JustSomeGuy {
myProps(): string[] {
return ["Banana"];
}
myName(): string {
return 'Bob';
}
}
class Charlie extends JustSomeGuy {
myProps(): string[] {
return ["Apple", "Banana"];
}
myName(): string {
return 'Charlie';
}
}
const FruitBasket:{ [prop: string]: JustSomeGuy[]} = {}
const myAlice = new Alice();
const myBob = new Bob();
const myCharlie = new Charlie();
const Objects = [myAlice, myBob, myCharlie];
for (const currentObject of Objects) {
const fruits = currentObject.myProps();
fruits.forEach( (v) => { (FruitBasket[v] ??= []).push(currentObject);});
}
for (let key in FruitBasket){
let value = FruitBasket[key];
for (let i = 0; i < value.length; i++){
console.log("In key: " + key + " the ith element [i = " + i + "] is: " + value[i].myName() );
}
}
I believe that this is what I want. Marking this as resolved.
Let's start with the types of the data structures that you described:
type ObjWithProps = {
getProperties (): string[];
};
type PropertyHolders = {
[key: string]: ObjWithProps[] | undefined;
};
// Could also be written using built-in type utilities like this:
// type PropertyHolders = Partial<Record<string, string[]>>;
The type ObjWithProps has a method which returns an array of string elements.
The type PropertyHolders is an object type that is indexed by string values (keys), and each value type is an array of ObjWithProps (if it exists, or undefined if it doesn't) — no object has a value at every possible key.
Next, let's replicate the data structures you showed in your example:
const HoA: PropertyHolders = {};
const currentObject: ObjWithProps = {
getProperties () {
return ['p1', 'p2', 'p3' /* etc. */];
}
};
const currentProperties = currentObject.getProperties();
In the code above, the currentObject has some arbitrary properties (p1, p2, p3). This is just to have reproducible example data. Your own implementation will likely be different, but the types are the same.
Finally, let's look at the part where you assign the values to the hash map:
currentProperties.forEach((v) => {
HoA[v].push(currentObject); /*
~~~~~~
Object is possibly 'undefined'.(2532) */
});
You can see that there's a compiler error where you try to access the array at the key v. Because you aren't sure that the array exists (no object has a value at every key), trying to invoke a push method on undefined would throw a runtime error. TypeScript is trying to help you prevent that case.
Instead, you can use the nullish coalescing assignment operator (??=) to ensure that the array is created (if it doesn't already exist) before pushing in a new value. This is what that refactor would look like:
currentProperties.forEach((v) => {
(HoA[v] ??= []).push(currentObject); // ok
});
Full code in TS Playground
Utility types references:
Record<Keys, Type>
Partial<Type>

Extending Array in TypeScript for specific type

I know how to extend array for any type:
declare global {
interface Array<T> {
remove(elem: T): Array<T>;
}
}
if (!Array.prototype.remove) {
Array.prototype.remove = function<T>(this: T[], elem: T): T[] {
return this.filter(e => e !== elem);
}
}
Source: Extending Array in TypeScript
But is there also a way to extend the array only for a specific type?. Like only for arrays of type User -> Array<User>.
I want to create a extend method, like .toUsersMap() and this method should not be displayed for arrays, which have not the type User.
You can achieve similar behaviour:
type User = {
tag: 'User'
}
interface Array<T> {
toUsersMap: T extends User ? (elem: T) => Array<T> : never
}
declare var user: User;
const arr = [user]
arr.toUsersMap(user) // ok
const another_array = [{ notUser: true }]
another_array.toUsersMap() // This expression is not callable
If T parameter does not extends User, TS will disallow using toUsersMap
Playground
I don't think there's a way to completely suppress the IntelliSense prompting for toUsersMap() on Arrays, but you can definitely make it a compiler error to call arr.toUsersMap() unless arr is assignable to Array<User>. One way to do this is to give the toUsersMap() method a this parameter:
interface Array<T> {
toUsersMap(this: Array<User>): Map<string, User>;
}
Now the compiler will require that toUsersMap() only be called with a this context of something assignable to Array<User>:
interface User {
username: string;
}
const arr = [{ username: "foo" }, { username: "bar" }];
arr.toUsersMap() // okay, no error
const another_array = ["hello", 123];
another_array.toUsersMap() // error,
//~~~~~~~~~~~ <--
// The 'this' context of type '{ notUser: boolean; }[]' is
// not assignable to method's 'this' of type 'User[]'
Playground link to code

Typescript says array.pop() might return undefined even when array is guaranteed to contain elements

Why does Typescript warn me that pop() might return undefined even after I've verified that I'm removing an element from a non-empty array?
type Person = {
name: string;
};
const people: Person[] = [{ name: 'Sandra' }];
if (people.length) {
const { name } = people.pop();
// TS Error: Property 'name' does not exist on type 'Person | undefined'.
}
Even if I make the guarantee more explicit, I get the same error:
if (people.length > 0 && people[people.length - 1] !== undefined){
const { name } = people.pop();
// TS Error: Property 'name' does not exist on type 'Person | undefined'.
}
See microsoft/TypeScript#30406 for a canonical answer.
For the question, "why does the compiler think that pop() might return undefined even when I check the array's length", the short answer is "because the TypeScript standard library's call signature for the pop() method on Array<T> returns T | undefined":
interface Array<T> {
// ... elided
pop(): T | undefined;
// ... elided
}
Thus whenever you call pop() on an array, the type of the return value will include undefined.
The logical next question is "why don't they make better call signatures which return T if the length is nonzero and undefined if the length is zero?" The answer to that is "because checking the length property of a general array type doesn't change the apparent type of the array, so the call signatures can't tell the difference".
You could, for example, add a few call signatures like this:
interface Array<T> {
pop(this: { length: 0 }): undefined;
pop(this: { 0: T }): T;
}
By using this parameters, each call signature will only be selected when the array on which the method is called matches the specified type. If the array has a length of 0, return undefined. If the array has an element of type T at the key 0, return T.
This will work well for tuple types whose length is fixed and whose element indices are known:
declare const u: [];
const a = u.pop(); // undefined
declare const v: [1, 2];
const b = v.pop(); // 1 | 2
But of course calling pop() on a tuple is a bad idea and not the kind of thing you'd be likely to want. If you have a variable-length array and call pop() on it, neither call signature is selected and you fall back to the built-in T | undefined, even if you try to check the length:
const w = Math.random() < 0.5 ? [] : ["a"] // string[]
if (w.length) {
w.pop().toUpperCase(); // error! Object is possibly undefined
}
The problem is that the length property of an Array<T> is of type number, and there's no "nonzero number" type to narrow w.length to. In order to support this kind of thing, you'd need something like negated types which are not part of TypeScript. It's possible that with sufficient compiler work, someone could give enough structure to arrays in TypeScript that a truthiness check on w.length would narrow down the type of the array to something you can call pop() on without worrying about getting undefined out.
But it would add a ton of complexity to the compiler and the language to support this use case. The benefit is not likely to outweigh the cost.
In the absence of this, it's so much easier for you to skip the length check and just call pop(), checking to see whether it's undefined or not. This is the same amount of work for you, since it's just moving the check from before to after the call, and it makes things more straightforward for the compiler.
The other answers posted here suggest this and other workarounds, so I won't go into them. My main point is that the language is just not equipped to allow a length check to affect the behavior of pop(). Or, as #RyanCavanaugh (dev lead of TS) commeted in microsoft/TypeScript#30406,
This isn't something we're capable of tracking
Oh well!
Playground link to code
You can do it this way.
type Person = {
name: string;
};
const people: Person[] = [{ name: 'Sandra' }];
if (people.length) {
const nextPerson = people.pop() ;
if(nextPerson){
const {name} =nextPerson;
}
}
Solution-2 (! after pop)
type Person = {
name: string;
};
const people: Person[] = [{ name: 'Sandra' }];
if (people.length) {
const {name}=people.pop()!;
console.log(name);
}
You can use the as keyword to assert that the pop() is returning a string like this:
if (people.length) {
const { name } = people.pop() as string;
}
This tells Typescript pop() is returning a string and not undefined.
Why don't you simply use pop() and check the existence of value. The pop method returns undefined when the array is empty.
type Person = {
name: string;
};
const people: Person[] = [{ name: 'Sandra' }];
let person = people.pop();
if (person) {
const { name } = person;
}
You can either use "as" keyword or "!" to asser that array.pop() is not returning non-null or non-undefined values.
// We are telling the compiler that the returned value will always be a string
if (people.length) {
const { name } = people.pop() as string;
}
// We are telling compiler that the value will never be null or undefined
if (people.length) {
const { name } = people.pop()!;
}
for more information
type Person = {
name: string;
};
const people: Person[] = [{ name: 'Sandra' }];
if (people.length) {
let popped = people.pop();
console.log(popped);
}
Output:
[LOG]: {
"name": "Sandra"
}
It solves the problem, then you can get the name. But it could be that typescript couldn't map the returned value to the object { name }

Typescript Assertion on nested object [duplicate]

I receive a JSON object from an AJAX call to a REST server. This object has property names that match my TypeScript class (this is a follow-on to this question).
What is the best way to initialize it? I don't think this will work because the class (& JSON object) have members that are lists of objects and members that are classes, and those classes have members that are lists and/or classes.
But I'd prefer an approach that looks up the member names and assigns them across, creating lists and instantiating classes as needed, so I don't have to write explicit code for every member in every class (there's a LOT!)
These are some quick shots at this to show a few different ways. They are by no means "complete" and as a disclaimer, I don't think it's a good idea to do it like this. Also the code isn't too clean since I just typed it together rather quickly.
Also as a note: Of course deserializable classes need to have default constructors as is the case in all other languages where I'm aware of deserialization of any kind. Of course, Javascript won't complain if you call a non-default constructor with no arguments, but the class better be prepared for it then (plus, it wouldn't really be the "typescripty way").
Option #1: No run-time information at all
The problem with this approach is mostly that the name of any member must match its class. Which automatically limits you to one member of same type per class and breaks several rules of good practice. I strongly advise against this, but just list it here because it was the first "draft" when I wrote this answer (which is also why the names are "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);
Option #2: The name property
To get rid of the problem in option #1, we need to have some kind of information of what type a node in the JSON object is. The problem is that in Typescript, these things are compile-time constructs and we need them at runtime – but runtime objects simply have no awareness of their properties until they are set.
One way to do it is by making classes aware of their names. You need this property in the JSON as well, though. Actually, you only need it in the 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);
Option #3: Explicitly stating member types
As stated above, the type information of class members is not available at runtime – that is unless we make it available. We only need to do this for non-primitive members and we are good to go:
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);
Option #4: The verbose, but neat way
Update 01/03/2016: As #GameAlchemist pointed out in the comments (idea, implementation), as of Typescript 1.7, the solution described below can be written in a better way using class/property decorators.
Serialization is always a problem and in my opinion, the best way is a way that just isn't the shortest. Out of all the options, this is what I'd prefer because the author of the class has full control over the state of deserialized objects. If I had to guess, I'd say that all other options, sooner or later, will get you in trouble (unless Javascript comes up with a native way for dealing with this).
Really, the following example doesn't do the flexibility justice. It really does just copy the class's structure. The difference you have to keep in mind here, though, is that the class has full control to use any kind of JSON it wants to control the state of the entire class (you could calculate things etc.).
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);
you can use Object.assign I don't know when this was added, I'm currently using Typescript 2.0.2, and this appears to be an ES6 feature.
client.fetch( '' ).then( response => {
return response.json();
} ).then( json => {
let hal : HalJson = Object.assign( new HalJson(), json );
log.debug( "json", hal );
here's HalJson
export class HalJson {
_links: HalLinks;
}
export class HalLinks implements Links {
}
export interface Links {
readonly [text: string]: Link;
}
export interface Link {
readonly href: URL;
}
here's what chrome says it is
HalJson {_links: Object}
_links
:
Object
public
:
Object
href
:
"http://localhost:9000/v0/public
so you can see it doesn't do the assign recursively
TLDR: TypedJSON (working proof of concept)
The root of the complexity of this problem is that we need to deserialize JSON at runtime using type information that only exists at compile time. This requires that type-information is somehow made available at runtime.
Fortunately, this can be solved in a very elegant and robust way with decorators and ReflectDecorators:
Use property decorators on properties which are subject to serialization, to record metadata information and store that information somewhere, for example on the class prototype
Feed this metadata information to a recursive initializer (deserializer)
Recording Type-Information
With a combination of ReflectDecorators and property decorators, type information can be easily recorded about a property. A rudimentary implementation of this approach would be:
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);
}
For any given property, the above snippet will add a reference of the constructor function of the property to the hidden __propertyTypes__ property on the class prototype. For example:
class Language {
#JsonMember // String
name: string;
#JsonMember// Number
level: number;
}
class Person {
#JsonMember // String
name: string;
#JsonMember// Language
language: Language;
}
And that's it, we have the required type-information at runtime, which can now be processed.
Processing Type-Information
We first need to obtain an Object instance using JSON.parse -- after that, we can iterate over the entires in __propertyTypes__ (collected above) and instantiate the required properties accordingly. The type of the root object must be specified, so that the deserializer has a starting-point.
Again, a dead simple implementation of this approach would be:
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);
The above idea has a big advantage of deserializing by expected types (for complex/object values), instead of what is present in the JSON. If a Person is expected, then it is a Person instance that is created. With some additional security measures in place for primitive types and arrays, this approach can be made secure, that resists any malicious JSON.
Edge Cases
However, if you are now happy that the solution is that simple, I have some bad news: there is a vast number of edge cases that need to be taken care of. Only some of which are:
Arrays and array elements (especially in nested arrays)
Polymorphism
Abstract classes and interfaces
...
If you don't want to fiddle around with all of these (I bet you don't), I'd be glad to recommend a working experimental version of a proof-of-concept utilizing this approach, TypedJSON -- which I created to tackle this exact problem, a problem I face myself daily.
Due to how decorators are still being considered experimental, I wouldn't recommend using it for production use, but so far it served me well.
I've created a tool that generates TypeScript interfaces and a runtime "type map" for performing runtime typechecking against the results of JSON.parse: ts.quicktype.io
For example, given this JSON:
{
"name": "David",
"pets": [
{
"name": "Smoochie",
"species": "rhino"
}
]
}
quicktype produces the following TypeScript interface and type map:
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",
},
};
Then we check the result of JSON.parse against the type map:
export function fromJson(json: string): Person {
return cast(JSON.parse(json), object("Person"));
}
I've left out some code, but you can try quicktype for the details.
I've been using this guy to do the job: https://github.com/weichx/cerialize
It's very simple yet powerful. It supports:
Serialization & deserialization of a whole tree of objects.
Persistent & transient properties on the same object.
Hooks to customize the (de)serialization logic.
It can (de)serialize into an existing instance (great for Angular) or generate new instances.
etc.
Example:
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);
For simple objects, I like this method:
class Person {
constructor(
public id: String,
public name: String,
public title: String) {};
static deserialize(input:any): Person {
return new Person(input.id, input.name, input.title);
}
}
var person = Person.deserialize({id: 'P123', name: 'Bob', title: 'Mr'});
Leveraging the ability to define properties in the constructor lets it be concise.
This gets you a typed object (vs all the answers that use Object.assign or some variant, which give you an Object) and doesn't require external libraries or decorators.
This is my approach (very simple):
const jsonObj: { [key: string]: any } = JSON.parse(jsonStr);
for (const key in jsonObj) {
if (!jsonObj.hasOwnProperty(key)) {
continue;
}
console.log(key); // Key
console.log(jsonObj[key]); // Value
// Your logic...
}
if you want type safety and don't like decorators
abstract class IPerson{
name?: string;
age?: number;
}
class Person extends IPerson{
constructor({name, age}: IPerson){
super();
this.name = name;
this.age = age;
}
}
const json = {name: "ali", age: 80};
const person = new Person(json);
or this which I prefer
class Person {
constructor(init?: Partial<Person>){
Object.assign(this, init);
}
name?: string;
age?: number;
}
const json = {name: "ali", age: 80};
const person = new Person(json);
Option #5: Using Typescript constructors and jQuery.extend
This seems to be the most maintainable method: add a constructor that takes as parameter the json structure, and extend the json object. That way you can parse a json structure into the whole application model.
There is no need to create interfaces, or listing properties in constructor.
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;
}
}
In your ajax callback where you receive a company to calculate salaries:
onReceiveCompany( jsonCompany : any )
{
let newCompany = new Company( jsonCompany );
// call the methods on your newCompany object ...
newCompany.calculateSalaries()
}
The best I found for this purpose is the class-transformer
That's how you use it:
Some class:
export class Foo {
name: string;
#Type(() => Bar)
bar: Bar;
public someFunction = (test: string): boolean => {
...
}
}
// the docs say "import [this shim] in a global place, like app.ts"
import 'reflect-metadata';
// import this function where you need to use it
import { plainToClass } from 'class-transformer';
export class SomeService {
anyFunction() {
u = plainToClass(Foo, JSONobj);
}
}
If you use the #Type decorator nested properties will be created, too.
The 4th option described above is a simple and nice way to do it, which has to be combined with the 2nd option in the case where you have to handle a class hierarchy like for instance a member list which is any of a occurences of subclasses of a Member super class, eg Director extends Member or Student extends Member. In that case you have to give the subclass type in the json format
JQuery .extend does this for you:
var mytsobject = new mytsobject();
var newObj = {a:1,b:2};
$.extend(mytsobject, newObj); //mytsobject will now contain a & b
Another option using factories
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
use like this
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);
keeps your classes simple
injection available to the factories for flexibility
I personally prefer option #3 of #Ingo Bürk.
And I improved his codes to support an array of complex data and Array of primitive data.
interface IDeserializable {
getTypes(): Object;
}
class Utility {
static deserializeJson<T>(jsonObj: object, classType: any): T {
let instanceObj = new classType();
let types: IDeserializable;
if (instanceObj && instanceObj.getTypes) {
types = instanceObj.getTypes();
}
for (var prop in jsonObj) {
if (!(prop in instanceObj)) {
continue;
}
let jsonProp = jsonObj[prop];
if (this.isObject(jsonProp)) {
instanceObj[prop] =
types && types[prop]
? this.deserializeJson(jsonProp, types[prop])
: jsonProp;
} else if (this.isArray(jsonProp)) {
instanceObj[prop] = [];
for (let index = 0; index < jsonProp.length; index++) {
const elem = jsonProp[index];
if (this.isObject(elem) && types && types[prop]) {
instanceObj[prop].push(this.deserializeJson(elem, types[prop]));
} else {
instanceObj[prop].push(elem);
}
}
} else {
instanceObj[prop] = jsonProp;
}
}
return instanceObj;
}
//#region ### get types ###
/**
* check type of value be string
* #param {*} value
*/
static isString(value: any) {
return typeof value === "string" || value instanceof String;
}
/**
* check type of value be array
* #param {*} value
*/
static isNumber(value: any) {
return typeof value === "number" && isFinite(value);
}
/**
* check type of value be array
* #param {*} value
*/
static isArray(value: any) {
return value && typeof value === "object" && value.constructor === Array;
}
/**
* check type of value be object
* #param {*} value
*/
static isObject(value: any) {
return value && typeof value === "object" && value.constructor === Object;
}
/**
* check type of value be boolean
* #param {*} value
*/
static isBoolean(value: any) {
return typeof value === "boolean";
}
//#endregion
}
// #region ### Models ###
class Hotel implements IDeserializable {
id: number = 0;
name: string = "";
address: string = "";
city: City = new City(); // complex data
roomTypes: Array<RoomType> = []; // array of complex data
facilities: Array<string> = []; // array of primitive data
// getter example
get nameAndAddress() {
return `${this.name} ${this.address}`;
}
// function example
checkRoom() {
return true;
}
// this function will be use for getting run-time type information
getTypes() {
return {
city: City,
roomTypes: RoomType
};
}
}
class RoomType implements IDeserializable {
id: number = 0;
name: string = "";
roomPrices: Array<RoomPrice> = [];
// getter example
get totalPrice() {
return this.roomPrices.map(x => x.price).reduce((a, b) => a + b, 0);
}
getTypes() {
return {
roomPrices: RoomPrice
};
}
}
class RoomPrice {
price: number = 0;
date: string = "";
}
class City {
id: number = 0;
name: string = "";
}
// #endregion
// #region ### test code ###
var jsonObj = {
id: 1,
name: "hotel1",
address: "address1",
city: {
id: 1,
name: "city1"
},
roomTypes: [
{
id: 1,
name: "single",
roomPrices: [
{
price: 1000,
date: "2020-02-20"
},
{
price: 1500,
date: "2020-02-21"
}
]
},
{
id: 2,
name: "double",
roomPrices: [
{
price: 2000,
date: "2020-02-20"
},
{
price: 2500,
date: "2020-02-21"
}
]
}
],
facilities: ["facility1", "facility2"]
};
var hotelInstance = Utility.deserializeJson<Hotel>(jsonObj, Hotel);
console.log(hotelInstance.city.name);
console.log(hotelInstance.nameAndAddress); // getter
console.log(hotelInstance.checkRoom()); // function
console.log(hotelInstance.roomTypes[0].totalPrice); // getter
// #endregion
Maybe not actual, but simple solution:
interface Bar{
x:number;
y?:string;
}
var baz:Bar = JSON.parse(jsonString);
alert(baz.y);
work for difficult dependencies too!!!
you can do like below
export interface Instance {
id?:string;
name?:string;
type:string;
}
and
var instance: Instance = <Instance>({
id: null,
name: '',
type: ''
});
My approach is slightly different. I do not copy properties into new instances, I just change the prototype of existing POJOs (may not work well on older browsers). Each class is responsible for providing a SetPrototypes method to set the prototoypes of any child objects, which in turn provide their own SetPrototypes methods.
(I also use a _Type property to get the class name of unknown objects but that can be ignored here)
class ParentClass
{
public ID?: Guid;
public Child?: ChildClass;
public ListOfChildren?: ChildClass[];
/**
* Set the prototypes of all objects in the graph.
* Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
* #param pojo Plain object received from API/JSON to be given the class prototype.
*/
private static SetPrototypes(pojo: ParentClass): void
{
ObjectUtils.SetPrototypeOf(pojo.Child, ChildClass);
ObjectUtils.SetPrototypeOfAll(pojo.ListOfChildren, ChildClass);
}
}
class ChildClass
{
public ID?: Guid;
public GrandChild?: GrandChildClass;
/**
* Set the prototypes of all objects in the graph.
* Used for recursive prototype assignment on a graph via ObjectUtils.SetPrototypeOf.
* #param pojo Plain object received from API/JSON to be given the class prototype.
*/
private static SetPrototypes(pojo: ChildClass): void
{
ObjectUtils.SetPrototypeOf(pojo.GrandChild, GrandChildClass);
}
}
Here is ObjectUtils.ts:
/**
* ClassType lets us specify arguments as class variables.
* (where ClassType == window[ClassName])
*/
type ClassType = { new(...args: any[]): any; };
/**
* The name of a class as opposed to the class itself.
* (where ClassType == window[ClassName])
*/
type ClassName = string & {};
abstract class ObjectUtils
{
/**
* Set the prototype of an object to the specified class.
*
* Does nothing if source or type are null.
* Throws an exception if type is not a known class type.
*
* If type has the SetPrototypes method then that is called on the source
* to perform recursive prototype assignment on an object graph.
*
* SetPrototypes is declared private on types because it should only be called
* by this method. It does not (and must not) set the prototype of the object
* itself - only the protoypes of child properties, otherwise it would cause a
* loop. Thus a public method would be misleading and not useful on its own.
*
* https://stackoverflow.com/questions/9959727/proto-vs-prototype-in-javascript
*/
public static SetPrototypeOf(source: any, type: ClassType | ClassName): any
{
let classType = (typeof type === "string") ? window[type] : type;
if (!source || !classType)
{
return source;
}
// Guard/contract utility
ExGuard.IsValid(classType.prototype, "type", <any>type);
if ((<any>Object).setPrototypeOf)
{
(<any>Object).setPrototypeOf(source, classType.prototype);
}
else if (source.__proto__)
{
source.__proto__ = classType.prototype.__proto__;
}
if (typeof classType["SetPrototypes"] === "function")
{
classType["SetPrototypes"](source);
}
return source;
}
/**
* Set the prototype of a list of objects to the specified class.
*
* Throws an exception if type is not a known class type.
*/
public static SetPrototypeOfAll(source: any[], type: ClassType): void
{
if (!source)
{
return;
}
for (var i = 0; i < source.length; i++)
{
this.SetPrototypeOf(source[i], type);
}
}
}
Usage:
let pojo = SomePlainOldJavascriptObjectReceivedViaAjax;
let parentObject = ObjectUtils.SetPrototypeOf(pojo, ParentClass);
// parentObject is now a proper ParentClass instance
**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;
}
}

Resources