I learnt typescript in a hurry and still a newbie. Coming from a functional background I am used to seperate object data from its methods. For example let us consider a simple data object that i will want to augment as my code base grow :
export interface Data {
readonly value: number;
}
export function Data(value: number): Data {
return { value };
}
const data: Data = Data(2);
console.info(data); //output { value : 2 }
Data is a central brick of my code since it contains all the data defining a core object of my business model. My application growing in feature I want to augment it with dedicated methods.
I thought of two approaches.
I can use the "companion object" pattern :
export namespace Data {
export function incr(self: Data): Data {
return Data(self.value + 1);
}
}
console.info(data); //output { value : 2 }
Data.incr(data);
With this approach data stay a simple data object, and I can enrich it. However, each time I will use it I will have a bloated syntax
I can use the interface pattern :
export namespace Data {
export function Ops(self: Data) {
return {
self : self,
incr(): Data(self.value + 1);
}
}
}
const data_withops = Data.Ops({ value : 2 });
console.info(data_withops.self); //output { value : 2 }
data_withops.incr()
It is less bloated but I still have to manually encapsulate/decapsulate the data.
I can inject the operators into the data
export interface Data {
readonly value: number,
incr(): Data
}
const data: Data = Data(2);
console.info(data); //output { value : 2, incr : Function }
But then the object is bloated, bloating my console, my cache and my network.
Is it possible to do it without any of those problems?
- data is not bloated : console.info(data); //output { value : 2 }
- user of the object does not have to care about "loading/unloading" the operators
- user has a light syntax : "data.incr()" is defined
I thought it would works with the prototypes but I didn’t succeed
declare global {
export interface Data {
incr(this: Data): number;
}
}
Data.prototype.incr = function (this: Data): Data {
return Data(this.value + 1);
};
console.info(data); //output { value : 2 }
data.incr();
2
Answers
This feels like it’s begging to be a class.
If by "separate" an object from its methods all you mean is that the methods shouldn’t be added individually to each instance, then by far the most common way to do things like this is with a
class
, which automatically happens via prototypical inheritance:If you really need to "separate" it even further and put the methods in some separate file, you can just add these methods to the class prototype and use declaration merging to let TypeScript know about it:
In both of these cases you are mutating the state of your class instances (e.g.,
this.value = value
). If you really need immutable(ish) functional data structures then you can still use prototypical inheritance, but you’ll find yourself jumping through hoops like manually callingObject.create()
:This doesn’t seem like an obvious improvement. The usual approach is to just use
class
declarations until and unless you have some strong reason not to.Playground link to code