Angular 6 ES6 initiate object arrays - javascript

I'm really new to javascript.
I have a nested class structure where I need to initiate using a json object. My question is how can I initiate array of EventDate objects and assign to this.dates in CustomerEvents constructor
export default class CustomerEvent {
constructor(customer_event: any) {
this.event_title = customer_event.event_title;
this.customer = customer_event.customer;
this.total_budget = customer_event.total_budget;
this.no_of_people = customer_event.no_of_people;
this.dates = /**array of new EventDate(customer_event.dates) **/;
}
event_title: string;
customer: Customer;
total_budget: number;
no_of_people: number;
dates: EventDate[];
}
class EventDate {
constructor(date: any) {
this.start_date = date.start_date;
this.end_date = date.end_date;
}
start_date: Date;
end_date: Date;
}
If someone could help me on this, it'll be really helpful. Thanks

Just assign new empty array, like this:
constructor(customer_event: any) {
this.event_title = customer_event.event_title;
this.customer = customer_event.customer;
this.total_budget = customer_event.total_budget;
this.no_of_people = customer_event.no_of_people;
this.dates = [];
}
If you need to cast incoming array, you can do this:
...
this.dates = customer_event.dates.map(date => new EventDate(date));
...

Angular Style Guide recommends using interfaces for data model instead of classes:
Consider using an interface for data models.
That being said, you can refactor your code like this:
export interface EventDate {
start_date: Date;
end_date: Date;
}
export interface CustomerEvent {
event_title: string;
customer: Customer;
total_budget: number;
no_of_people: number;
dates: EventDate[];
}
Now when it comes to initialization, you can do it something like this:
const customerEvent: CustomerEvent = {
event_title: 'Some Title',
customer: { /*An Object representing a Customer Type*/ }
total_budget: 123,
no_of_people: 456,
dates: [{
start_date: new Date(),
end_date: new Date()
}]
};

Create those instances yourself:
constructor(customer_event: any) {
this.event_title = customer_event.event_title;
this.customer = customer_event.customer;
this.total_budget = customer_event.total_budget;
this.no_of_people = customer_event.no_of_people;
this.dates = customer_event.dates.map(date => new EventDate(date));
}

Related

Create map like data structure

I have this product model:
export class Product {
id: number;
name: string;
price: number;
url: string;
description: string;
constructor() {
this.id = 1;
this.name = '';
this.price = 0.0;
this.url = '';
this.description = '';
}
}
I would like to create a map like a data structure where I can use the Product id as a key and with that, I can receive the structure that has the product itself and the quantity.
In Java that would be:
Map<Integer, Pair<Product, Integer>> = new HashMap<>();
How do I implement it using TypeScript?
You can use the Record:
type ProductMap = Record<number, {product: Product, quantity: number}>;
How about
type ProductMap = Map<number, {product: Product, quantity: number}>
const myProductMap : ProductMap = new Map();
// or use it directly
const myProductMap2 = new Map<number, {product: Product, quantity: number}>();
You can create custom dictionary like this:
interface Map<T> {
[Key: number]: KeyValuePair<T,number>;
}
interface KeyValuePair<T, Y> {
key: T;
value: Y;
}
var somedata: Map<Product> = {...};

Javascript: How to add properties to a class object that are part of class?

I have been thinking if this could be done. If yes, how? I am fairly new to javaScript world so please bear with me.
To best explain what I am trying to achieve, I will provide a small example:
PLAYGROUND
class Document {
basicDetails: BasicDetails;
constructor(objDocument:any) {
this.basicDetails = new BasicDetails(objDocument);
//this.basicDetails = Object.assign(this.basicDetails, objDocument.basicDetails);
}
}
class BasicDetails {
type: string;
createdDate: Date;
constructor(objDocument:any) {
this.type = "";
this.createdDate = new Date();
}
}
As you can see, the Document Class has just one property called basicDetails. But the objDocument that it's constructor accepts has not only basicDetails object but also other properties.
let obj = {basicDetails: {type: "draft"}, lineDetails:{lineid:123,itemName:"itemName"} }
let doc = new Document(obj)
console.log(doc)
The output of the above code is naturally:
{
"basicDetails": {
"type": "",
"createdDate": "2020-12-03T08:02:59.780Z"
}
}
But I want the output to be:
{
"basicDetails": {
"type": "",
"createdDate": "2020-12-03T08:02:59.780Z"
},
"lineDetails":{"lineid":123,"itemName":"itemName"} }
}
Whatever properties that are part of objDocument:any in addition to basicDetails must be added automatically as is. And the properties should not have to be added out side of the class.
let doc = new Document(obj)
doc["lineDetails] = obj.lineDetials // not acceptable
You could extract the basic details from the object that is passed to the Document constructor, and identify what is all the rest, using destructuring with the spread syntax.
In this demo I commented out the typescript parts, so it can run here:
class Document {
basicDetails /*: BasicDetails */;
constructor(objDocument /*:any*/) {
let {basicDetails, ...rest} = objDocument;
this.basicDetails = new BasicDetails(basicDetails);
Object.assign(this, rest);
}
}
class BasicDetails {
type /*: string */;
createdDate /*: Date */;
constructor(objDocument /*:any */) {
this.type = objDocument.type;
this.createdDate = new Date();
}
}
let obj = {basicDetails: {type: "draft"}, lineDetails:{lineid:123,itemName:"itemName"} };
let doc = new Document(obj);
console.log(doc);
This is an improvement from #trincot answer. First of all the design is not the best, you should inject an already created class into your Document constructor.
class Document {
basicDetails /*: BasicDetails */;
constructor(basicDetails /*:BasicDetails*/, rest : /*:any*/) {
this.basicDetails = basicDetails
Object.assign(this, rest);
}
}
That is one of the SOLID principles ( Dependency Injection ).
let obj = { basicDetails: { type: "draft" }, lineDetails:{ lined : 123, itemName : "itemName" } };
class BasicDetails {
type /*: string */;
createdDate /*: Date */;
constructor(basicDetails /*:basicDetails */) {
this.type = basicDetails.type;
this.createdDate = new Date();
}
}
const { basicDetails, ...rest } = obj
const doc = new Document( new BasicDetails(basicDetails), rest )
You can first make your life easier by extracting the constructor options for Document. Since you want a basicDetails on the root level and then potentially anything else, this becomes:
interface DocumentOptions {
basicDetails: any,
[key: string]: unknown
}
This now means that you can get at least some type safety and prevent instantiating Document without basicDetails.
Now you can change the constructor to extract basicDetails for instantiating the BasicDetails object and then assign everything else to the current instance. Providing an [index signature] for Document signifies that it can have any amount of any properties. Making those properties readonly means that you're not allowed to change them:
class Document {
basicDetails: BasicDetails;
readonly [key: string]: unknown
constructor({ basicDetails, ...otherDetails } : DocumentOptions) {
this.basicDetails = new BasicDetails(basicDetails);
Object.assign(this, otherDetails);
}
}
You can further increase the type safety by assigning correct types for the constructor of BasicDetails then use the [ConstructorParameters] utility type to make sure the correct things are passed to Document:
interface DocumentOptions {
basicDetails: ConstructorParameters<typeof BasicDetails>[0],
[key: string]: unknown
}
class Document {
basicDetails: BasicDetails;
readonly [key: string]: unknown
constructor({ basicDetails, ...otherDetails } : DocumentOptions) {
this.basicDetails = new BasicDetails(basicDetails);
Object.assign(this, otherDetails);
}
}
class BasicDetails {
type: string;
createdDate: Date;
constructor(objDocument: {}) {
this.type = "";
this.createdDate = new Date();
}
}
let obj = { basicDetails: { type: "draft" }, lineDetails: { lineid: 123, itemName: "itemName" } }
let doc = new Document(obj)
doc["lineDetails"] = obj.lineDetials // not acceptable
console.log(doc)
If you want to be even more future proof, then you can accept the entire parameter list for BasicDetails, so if the constructor changes in the future, you don't need to change Document or DocumentOptions:
interface DocumentOptions {
basicDetails: ConstructorParameters<typeof BasicDetails>, //no index
[key: string]: unknown
}
class Document {
basicDetails: BasicDetails;
readonly [key: string]: unknown
constructor({ basicDetails, ...otherDetails } : DocumentOptions) {
this.basicDetails = new BasicDetails(...basicDetails);
//pass everything ^^^
Object.assign(this, otherDetails);
}
}
However, with that said, I'd advise collecting all other properties in a separate property instead, as that would make the Document API more stable:
interface DocumentOptions {
basicDetails: ConstructorParameters<typeof BasicDetails>[0],
[key: string]: unknown
}
class Document {
basicDetails: BasicDetails;
readonly otherDetails: Readonly<Record<string, unknown>>
constructor({ basicDetails, ...otherDetails }: DocumentOptions) {
this.basicDetails = new BasicDetails(basicDetails);
this.otherDetails = otherDetails;
}
serialise() {
return Object.assign({}, {basicDetails: this.basicDetails.serialise()}, this.otherDetails)
}
}
class BasicDetails {
type: string;
createdDate: Date;
constructor(objDocument: {}) {
this.type = "";
this.createdDate = new Date();
}
serialise() {
return { type: this.type };
}
}
let obj = { basicDetails: { type: "draft" }, lineDetails: { lineid: 123, itemName: "itemName" } }
let doc = new Document(obj)
doc.otherDetails["lineDetails"] = obj.lineDetails // not acceptable
console.log(doc.serialise())
I've added a serialise method for convenience - it will output the information as you expect:
{
"basicDetails": {
"type": "",
"createdDate": "2020-12-03T09:09:30.355Z"
},
"lineDetails": {
"lineid": 123,
"itemName": "itemName"
}
}
It also happens to give you cloning functionality for free, since it outputs the exact same data that the constructor consumes, so you can do this:
let obj = { basicDetails: { type: "draft" }, lineDetails: { lineid: 123, itemName: "itemName" } }
let doc1 = new Document(obj)
//free cloning
let doc2 = new Document(doc1.serialise());

TypeScript. Type error when creating new object with spread operator

class Todo {
private _date: Date;
public name: string;
constructor(todo: Todo) {
this._date = todo._date;
this.name = todo.name;
}
get date() { ... }
}
// example
const todos = Agent.fetchTodos();
const modifiedTodos1 = map.todos(todo => new Todo(todo)); // works fine
const modifiedTodos2 = map.todos(todo => new Todo( { ...todo, _date: new Date() } )); // doesn't work
The second modifiedTodos2 line of code gives me the following error: property date is missing in type [...]
In my scenario, I'm required to use the spread operator to extend the todo object with an additional prop. But I'm not sure why I'm seeing this error and how to deal with it...
and I also can't make the date optional with ? operator.
Thank you so much in advance for pointing out why my scenario doesn't work and what to do to make it work!
What you are trying to do is wrong because you do not consider encapsulation. _date is a private field that should not be accessed outside of the object. You defined the getter date, use it.
You should not use
this._date = todo._date;
but
this._date = todo.date;.
Here is an example :
class Todo {
private _date: Date;
public name: string;
constructor(todo) {
this._date = todo.date;
this.name = todo.name;
}
get date(): Date {
return this._date;
}
}
const todo = new Todo(null);
const modifiedTodos1 = new Todo(todo);
const modifiedTodos2 = new Todo({
...todo,
date: new Date(),
});
Also note that it will not work if you strongly type todo in the constructor like:
constructor(todo: Todo) {
this._date = todo.date;
this.name = todo.name;
}
Because typescript is waiting for a Todo object that you do not provide using the spread operator (no date function).
To adress this issue I recommend you to specify an interface that will only contains your setters data.
interface ITodo {
date: Date,
name: string,
}
class Todo {
private _date: Date;
public name: string;
constructor(todo: ITodo) {
this._date = todo.date;
this.name = todo.name;
}
get date(): Date {
return this._date;
}
}
const todo = new Todo(null);
const modifiedTodos1 = new Todo(todo);
const modifiedTodos2 = new Todo({
...todo,
date: new Date(),
});
In addition of previous answer. Except of interface, you can use types:
declare type TodoType ={
name:string,
date:Date
}
class Todo {
private _date: Date;
public name: string;
constructor(todo: TodoType) {
this._date = todo.date;
this.name = todo.name;
}
otherBehavior = ()=> {}
get date() { return this._date;}
}
const toDos:Array<Todo> = [];
const modifiedTodos2 = toDos.map(todo => new Todo( { ...todo, date: new Date() } ));
This way you can add additional behavior(properties/methods) to your Todo class and there will not be need to pass these in the object which you pass to create instance of Todo

Transformation of Observable

I've a data structure which looks like this:
Observable<Array<LineChart>>
whereby an LineChart is defined like
export interface LineChart {
name?: null | string;
series?: null | Array<DateLineChartEntry>;
}
and an DateLineChartEntry is defined like this:
export interface DateLineChartEntry {
name?: string;
value?: number;
}
where name is string, which contains Date.
For my follow-up operation with this DataStructure i need to convert the DateLineChartEntry to sth. like this:
export interface DateLineChartEntryConverted {
name?: Date;
value?: number;
}
which means, i've to map all DateLineChartEntries like this
DateLineChartEntry => {
name: new Date(name),
value: value
}
My current solutions looks like that:
this.data = getObservable({ body: parameters }).pipe(
map(lca => {
var lcaConverted = [];
for (var lc of lca) {
var name = lc.name
var lcN = {
name: name,
series: []
};
for (var e of lc.series) {
var n = new Date(e.name);
lcN.series.push({
name: n,
value: e.value
});
}
lcaConverted.push(lcN);
}
return lcaConverted;
})
);
Which is pretty ugly and I'm looking for a "nicer" solution to this.
Is there an easy way available to do this by using the initial Observable (and receiving an Observable as output)?
Thanks in advance for your help.
Are you just looking for something a little cleaner? If so look into some of the newer array features instead of writing for:
this.data = getObservable({
body: parameters
}).pipe(
map(lca => lca.map(entry => ({
name: entry.name,
series: entry.series.map(x => ({
name: new Date(x.name),
value: x.value
}))
}))
)
);
Specifically .map: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map

TypeScript Interface Undefined

I have this:
public getFriends() : IUser[] {
let friends: IUser[];
friends[0].Id = "test";
friends[0].Email = "asdasd";
return friends;
}
Could sound stupid, but why am I getting friends[0] undefined? What am I supposed to do if I don't have a class of type User in this case.
In this statement:
let friends: IUser[];
You are declaring friends and its type IUser[], you aren't initializing it yet, so friends's value is undefined at this point.
So, the first step is to initialize it:
let friends: IUser[] = [];
Now it is an array, you have to push some content into it. First you have to create a IUser:
// this is one (likely) way of creating a IUser instance, there may be others
let friend: IUser = {Id: "test", Email: "asdasd"};
And then add it to the array:
friends.push(friend);
So your final code would be:
public getFriends() : IUser[] {
let friends: IUser[] = [];
let friend: IUser = {Id: "test", Email: "asdasd"};
friends.push(friend);
return friends;
}

Categories