I am trying to have a small immutable class in typescript:
import * as _ from "lodash";
export class Immutable<T> {
constructor(public data:T) {
Object.freeze(data);
_.each(_.keysIn(data), (key) => {
Object.defineProperty(this, key, <PropertyDescriptor> {get: () => this.data[key]})
})
}
set(key:string, val:any):Immutable<T> {
if (_.isEqual(this.data[key], val)) {
return this;
}
let newData = {};
newData[key] = val;
return new Immutable<T>(_.defaults<T>(newData, this.data))
}
update(data:T) {
let newVal = _.defaults<T>(data, this.data);
return _.isEqual(this.data, newVal) ? this : new Immutable<T>(newVal);
}
get(key):any {
return this.data[key];
}
toJson():T {
return this.data;
}
}
Right now, I have to manually add T when creating an immutable like this const instance =<Immutable<{x: number}> & {x: number}> new Immutable({x: 1});, so that I can access x with instance.x. I know there is a way around it with defining new (): Immutable<T> & T as the constructor method somewhere, but I just don't find the resource I am remembering anymore. Anyone could point me in the right direction?
Thank you, Robin
Edit
interestingly enough, accessing properties of T via the immutable works now, though I truly don't understand why (does typescript understand the Object.defineProperty in the constructor?).
I updated the class to enable subclassing and setting default values there, if anyone is interested:
import * as _ from "lodash";
export class Immutable<T> {
constructor(public data:T) {
Object.freeze(data);
_.each(_.keysIn(data), (key) => {
Object.defineProperty(this, key, <PropertyDescriptor> {get: () => this.data[key]})
})
}
set(key:string, val:any):this {
if (_.isEqual(this.data[key], val)) {
return this;
}
const newData = _.defaults<T>(_.fromPairs([[key, val]]), this.data);
return this.new(newData)
}
update(data:T):this {
const newData = _.defaults<T>(data, this.data);
return _.isEqual(this.data, newData) ? this : this.new(newData);
}
new(...args:any[]):this {
return <this> (new (<any>this.constructor)(...args));
}
get(key):any {
return this.data[key];
}
toJson():T {
return this.data;
}
}
This makes s.th. like this possible:
class Child extends Immutable<{x:number}> {
constructor(data = {x: 1}) {
super(data)
}
}
I leave the question open, though, cause I'd still like to know the answer of how to make Typescript know that an exported class has more properties than defined (maybe added externally or via constructor like I did)
new Child({ x: 3 }).x?