Class with grouped properties in deeper levels - javascript

I'm trying to build a complex class where I want to group properties, making the instantiated object have multiple layers, instead of every property being at root level.
So far, the only way I've found to do this is by making a class with the properties to group, and then in a "parent" class add a property of the class I built.
The problem here though is that two properties not sharing the same class can't communicate with each other.
There are ways around this, but I find them all very hacky and looking bad. One would be to create a hidden element, and store data in there that a property from another class can read.
Another would be to create static properties, but then, unless you do some major work with that property, you can only have one object created from the parent class, as it'll be the same no matter the instantiation of the class.
Very basic example:
class A {
constructor(prop1){
this.property = prop1;
}
}
class B {
constructor(prop2){
this.property = prop2;
}
}
class C {
constructor(prop1, prop2){
this.PropertyA = new A(prop1);
this.PropertyB = new B(prop2);
}
}
let obj = new C(1, 1);
console.log(obj.PropertyA.property);
In this example, the property from class A can't get a value from property in class B.
So, my question is, is there another way of building the class C to keep the levels of hierarchy in the object?
I use the class structure because I like how it looks. It looks far more readable to me than the prototype structure, and I'm not building an object directly, as I would like to instantiate more of them.
It feels like I have forgotten things I've looked at to try to do this, but I'm sure it'll come to me soon enough after I post this.

Sooo...
I worked a bit on a static-solution, and basically made a private static property to hold a unique id per instantiated object, with the key-value pairs I want to be able to share between the different classes. This should only expose the methods to either set or get those values. The only requirement is that all the classes needs to be constructed with the object ID, so they can get the right value.
I understand that people will roll their eyes at my infantile tries to break the actual points of classes and such, but it works for me anyway in this specific circumstance anyway.
I'm sure there a multitude of ways to update it to ensure it runs more smoothly, but I think it works for most cases at the moment.
The code made in example code:
"use strict";
class A {
#id
#testProp
constructor(id){
this.#id = id;
this.#testProp = 10;
}
get TestProp(){ return this.#testProp + C.getSharedProp(this.#id, "BValue")};
set TestProp(newValue) { this.#testProp = newValue; C.setSharedProp(this.#id, "AValue", this.#testProp) };
}
class B {
#id
#testProp
constructor(id){
this.#id = id;
this.#testProp = 10;
}
get TestProp(){ return this.#testProp + C.getSharedProp(this.#id, "AValue")};
set TestProp(newValue) { this.#testProp = newValue; C.setSharedProp(this.#id, "BValue", this.#testProp) };
}
class C {
#id
constructor(){
this.#id = Math.random().toString(36).substr(2, 9);
this.PropertyA = new A(this.#id);
this.PropertyB = new B(this.#id);
}
static #sharedProps = {};
static getSharedProp(charId, valueName) {
if(!charId){
throw "Must supply character ID";
}
if(!valueName){
throw "Must supply name of value to return";
}
if(!(charId in this.#sharedProps)){
throw "Character ID not found";
}
if(!(valueName in this.#sharedProps[charId])){
throw valueName + "-element not found";
}
return this.#sharedProps[charId][valueName];
}
static setSharedProp(charId, valueName, value) {
if(!charId){
throw "Must supply character ID";
}
if(!valueName){
throw "Must supply name of value";
}
if(!(charId in this.#sharedProps)){
this.#sharedProps[charId] = [];
}
if(!(valueName in this.#sharedProps[charId])){
this.#sharedProps[charId][valueName] = -1;
}
if(!value){
console.warn("Value not supplied of " + valueName + ". Not updating extant value");
}else{
this.#sharedProps[charId][valueName] = value;
}
}
}
let obj = new C();
obj.PropertyA.TestProp = 20;
obj.PropertyB.TestProp = 5;
console.log(obj.PropertyA.TestProp); //should be 25; 20 from its own class and 5 from foreign class-object
console.log(obj.PropertyB.TestProp); //should be 25; 5 from its own class and 20 from foreign class-object

Related

How to create fields in a class using Symbol

I have class Movie. Movie constructor should provide a generation of unique product id within the application no matter how many products are created. You also need to define a field with the name of the movie. But according to the condition, for this I have to use Symbol data type. How can i do this?
class Movie {
constructor(name) {
//here I need to generate a unique id;
//here I need to define name fields
}
}
class Movie {
sym = Symbol('symbol description')
constructor(name) {
this.symInConstructor = Symbol('symbol description')
// this.sym != this.symInConstructor
// M1.sym == M2.sym <=> M1 == M2
}
}
// remeber, Symbol() != Symbol(), each call creates a unique one
However note that you can't ever serialize a Symbol.
If you need a serializable thingy, generate uuid or whatever
Also it may have sense to not use Symbol, but to use the class instance itself, I don't see how the Symbol may be used
Create a global constant, and use square brackets access
const MovieName = Symbol('a key for MovieName')
class Movie {
[MovieName] = "The Movie";
["myString"] = "is equivalent to:"
// myString = "is equivalent to:"
// and must be used for Symbols as
constructor() {
this[MovieName] = "The Movie in constructor"
}
// may keep is somewhere like this for usage in other places
static movieNameSymbol = MovieName
}

Javascript: allow certain classes to change properties of another class

I have Student class. The student has a grade.
Teacher class can change the student's grade.
Other classes (i.e., parent class) cannot do that. I think I get the part about how to change another class's property (please correct me if I'm wrong), but how to make sure that only the Teacher class can change the grade?
class Student {
grade = 'A';
changeGrade(grade) {
this.grade = grade
return `the new grade is ${this.grade}`
}
}
class Teacher {
changeStudentGrade(student, grade) {
return student.changeGrade(grade)
}
}
JavaScript does not support this functionality. However, you can still add suitable logic to facilitate it by checking the class that is trying to changeGrade within the method as follows:
changeGrade(grade, classChangingGrade) {
if (classChangingGrade instanceof Teacher) {
this.grade = grade
return `the new grade is ${this.grade}`
}
}
Then this method should be invoked as below:
return student.changeGrade(grade, this);
Disclaimer
Other classes can get around this by creating a new Teacher instance and invoking changeGrade as follows:
student.changeGrade(grade, new Teacher());

How can I stop JavaScript from passing a class by reference?

I have the following code in the constructor of a class (shortened for the purpose of the question):
constructor(effect: EffectInstance, names: string[], count?: number) {
this.effect = effect; // instance of a class "Effect"
let name; for (name of names) {
this.custom.set(name, this.effect); // custom: Map
}
}
EffectInstance is the type of this class, which is generic
When I change this.effect.name in a method of the class, or when I grab the effect from the Map this.custom and change its name, both are changed.
From what I can tell, this is due to JavaScript's pass-by-reference behavior with objects, as I'm 100% certain that I'm not modifying the values I don't want modified. (I'd like to be able to rename the Effect instance in the custom Map, but keep this.effect.name unchanged)
I tried to re-instantiate the classes with the parameters in constructor(), but this raises a new issue: I'd be losing types, and I can't seem to figure out how to work around this. Here's what I tried:
(EffectInstance, for reference: <EffectInstance extends Effect>)
constructor(effect: EffectInstance, names: string[], count?: number) {
this.effect = effect;
let altEffect = effect instanceof PlayerEffect ? new PlayerEffect(effect.name, effect.ignoreRaces) : new Effect(effect.name);
let name; for (name of names) {
this.custom.set(name, altEffect);
}
}
Doing so, TS raises this error on altEffect:
TS2345: Argument of type 'Effect' is not assignable to parameter of type 'EffectInstance'.   'Effect' is assignable to the constraint of type 'EffectInstance', but 'EffectInstance' could be instantiated with a different subtype of constraint 'Effect'.
I need to either stop the pass-by-reference behavior or preserve the type that EffectInstance contains. How can I do this?
Most languages pass objects by reference, so this is not a unique behavior with javascript.
If you want to pass a copy of the class instance and not the original instance and send the clone where you don't want send the original instance.
class Car {
constructor(name) {
this.name = name;
}
}
let orignalClass = new Car('BMW');
let cloneClass = Object.assign(Object.create(Object.getPrototypeOf(orignalClass)), orignalClass)
console.log(orignalClass);
console.log(cloneClass);
// now both can be updated individually
orignalClass.name = 'BMw-1';
cloneClass.name = "BMW-copy"
console.log("after update");
console.log(orignalClass);
console.log(cloneClass);
through your code, this.custom type should be Effect.
constructor(effect: EffectInstance, names: string[], count?: number) {
this.custom = new Map<string, Effect>(); // altEffect type could be PlayerEffect or Effect
this.effect = effect;
let altEffect = effect instanceof PlayerEffect ? new PlayerEffect(effect.name, effect.ignoreRaces) : new Effect(effect.name);
let name; for (name of names) {
this.custom.set(name, altEffect);
}
}

How to serialize an object and cast it back to the same class as the original object

I'm new to JavaScript so bear with me if what I'm asking is not "how you do it in JavaScript". Advice on other approaches are welcome.
I have a class named State and I need need to serialize objects of that class using JSON.stringify(). The next step is to deserialize them back into an objects. However, my class uses setters and getters.
The problem that I'm facing is that after I deserialized those objects the setters and getters seem to be gone. I just cannot figure out how I can properly turn serialized objects back into objects of that class so that they behave exactly the same as objects that are created using new directly.
In another language I would cast those objects into State objects. I cannot find a JavaScript mechanism that seems to work that way.
The code looks as follows:
class State {
constructor(href) {
this.url = href;
}
set url(href) {
this._url = new URL(href);
this.demoParam = this._url.searchParams.get("demoParam");
}
get url() {
return this._url;
}
set demoParam(value) {
let param = parseInt(value, 10);
if(isNaN(param)) {
param = 2;
}
console.log("Setting 'demoParam' to value " + param);
this._demoParam = param;
}
get demoParam() {
return this._demoParam;
}
toJSON() {
let stateObject = {};
const prototypes = Object.getPrototypeOf(this);
for(const key of Object.getOwnPropertyNames(prototypes)) {
const descriptor = Object.getOwnPropertyDescriptor(prototypes, key);
if(descriptor && typeof descriptor.get === 'function') {
stateObject[key] = this[key];
}
}
return stateObject;
}
}
let originalState = new State(window.location.href);
let newState1 = JSON.parse(JSON.stringify(originalState));
newState1.demoParam = 12;
let newState2 = Object.create(State.prototype, Object.getOwnPropertyDescriptors(JSON.parse(JSON.stringify(originalState))));
newState2.demoParam = 13;
let newState3 = Object.assign(new State(window.location.href), JSON.parse(JSON.stringify(originalState)));
newState3.demoParam = 14;
let newState4 = Object.setPrototypeOf(JSON.parse(JSON.stringify(originalState)), State.prototype);
newState4.demoParam = 15;
I would expect that everytime I set the demoParam property of a newStateX object I'd see a console log message. However. I only see it twice, i.e. for every new State(window.location.href) statement.
I have used the answer of this question. However, it does not work as expected.
when you serialize an object you trigger the toString or the toJSON method of your class' instance and end up with just a "dumb" JSON representation of your enumerable attributes.
if you want to recreate an instance that behaves like it did prior to serialisation, you will need to set an extra key/value pair in your toJSON function like ___internalType: 'state' and then later use eg. a switch statement to recreate your specific class with the new MyClass(serializedData)and passing in your serialised instance. Within the constructor of your class, you set all the attributes you need and voilà, you have your "old" instance again
/edit: to clarify the reason why your console logs aren't showing up is because you are not recreating an instance of your class but just creating a new plain Object.
You can use Object.assign to copy plain object data into an "empty" new instance of the class along the lines of this code:
function cast(o) {
if (!o._cls) return o;
var _cls = eval(o._cls);
return Object.assign(new _cls(), o);
}
In JavaScript i personally like to avoid using classes for my data objects. TypeScript offers some better opportunities to solve this problem, one of these is TypedJSON:
https://github.com/JohnWeisz/TypedJSON

Returning an item of type T from a list of classes that inherit from a base type - Typescript

I have a class (Foo) that is used to hold a list of items, the list of items of which all inherit from a base type (IBar), this list can contain any number of these items.
The issue I have is that I am trying to create a get method on Foo that takes a generic type restricted to types that inherit from IBar.
The interfaces and classes I have currently are:
interface IBar {
bar : string;
}
interface IFizz extends IBar {
buzz : string;
}
class Foo {
get<T extends IBar>() : T {
var item = this.list[0];
return item;
}
list : Array<IBar>;
}
With the code i'm trying to run being:
var foo = new Foo();
var item = foo.get<IFizz>();
I know in the above that the list is empty, but this is more trying to get the Typescript compiler not to show an error. The line that calls foo.get is fine and does not error, the problem is the get method itself.
The error i get from the above is "Type 'IBar' is not assignable to type 'T'".
If looking at C# for a reference the above would work (I believe) and would welcome any written examples to help me solve this.
Thanks
The above code would not work in C# either. Both language try to prevent you to do this, because this is not type safe. Consider the following code:
class Foo {
get<T extends IBar>() : T {
var item = this.list[0];
return item;
}
list : Array<IBar> = [ new OneClass() ];
}
var foo = new Foo();
var item = foo.get<AnotherClass>();
OneClass and AnotherClass both implement IBar, but list[0] is of type OneClass but the caller requests an instance of AnotherClass. So if the compiler would allow the code you wrote, you get item typed as AnotherClass but holding an instance of OneClass which may cause runtime errors.
Just like in C# you can force the typescript compiler to let you do this using a type assertion (or similarly a cast in C#). Although you can do this, you should have another mechanism for ensuring that the item in the array is actually of a correct type for T:
class Foo {
get<T extends IBar>() : T {
var item = this.list[0];
return item as T;
}
list : Array<IBar> = [];
}
var foo = new Foo();
var item = foo.get<IFizz>();
You're trying to assert that the item at index 0 is an IFizz, not just an IBar; that can't be assured statically given Foo's declaration, it can only be true at runtime with specific data. Although you could assert it (return item as T;), that's just hiding the problem: When code actually runs, the item at index 0 may not be an IFizz.
If the list is going to contain IFizz, it should be declared to do so. If it's going to contain any kind of IBar, it should be declared to do that, and any code using it for IFizz instances is responsible for ensuring that's really true.
Instead, Foo should be parameterized with the actual type of IBar it will contain:
class Foo<T extends IBar> {
get() : T {
var item = this.list[0];
return item;
}
list : Array<T>;
}
and then
var item = foo.get();

Categories