skip to Main Content

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


  1. This feels like it’s begging to be a class.

    class Data {
        value:number;
    
        constructor(value:number){
            this.value = value
        }
    
        increment(incrementAmount:number){
            this.value = this.value + incrementAmount
        }
    }
    
    Login or Signup to reply.
  2. 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:

    class Data {
        readonly value: number;
        constructor(value: number) {
            this.value = value;
        }
        incr(): Data {
            return new Data(this.value + 1);
        }
    }
    
    const data: Data = new Data(2);
    console.log(data); // {value: 2}
    const nextData = data.incr();
    console.log(nextData) // {value: 3}
    

    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:

    // original class
    class Data {
        readonly value: number;
        constructor(value: number) {
            this.value = value;
        }
    }
            
    // merge in a method in some other place
    interface Data {
        incr(): Data;
    }
    Data.prototype.incr = function (this: Data) {
        return new Data(this.value + 1)
    }
        
    const data: Data = new Data(2);
    console.log(data); // { value: 2 }
    const nextData = data.incr();
    console.log(nextData) // {value: 3}
    

    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 calling Object.create():

    // original class-like functions
    interface Data {
        readonly value: number
    }
    function Data(value: number): Data {
        return Object.create(Data.prototype, {
            value: { value: value, enumerable: true }
        })
    }
    
    
    // merge in a method in some other place
    interface Data {
        incr(): Data;
    }
    Data.prototype.incr = function (this: Data) {
        return Data(this.value + 1)
    }
    
    const data: Data = Data(2);
    console.log(data); // {value: 2}
    const nextData = data.incr();
    console.log(nextData) // {value: 3}
    

    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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search