Simple Fellow
Simple Fellow

Reputation: 4622

How to dynamically add methods to an object in typescript

I have the following class

class StoreBuilder<TStore> {
  private _map: Map<string, Function>
  private _selectors: Map<string, Function>
  
  constructor(){
    this._map = new Map<string, Function>()
    this._selectors =  new Map<string, Function>() 
  }


  add<TArg>(kind: string, code: (s:TStore, x:TArg)=>void){
    if(!this._map.has(kind)){
      const func = (store: TStore, a0: TArg)=>{
          code(store, a0)
      }
      this._map.set(kind, func)
    }

    if(!this._selectors.has(kind)){
      const func = (arg: TArg)=>{
        return {type: kind, payload: arg}
      }
      this._selectors.set(kind, func)
    }

  }

  build(){

    const reducer = (store: TStore, action: {type: string, payload: any})=>{
      if(this._map.has(action.type)){
       try {
        const code = this._map.get(action.type)
        code(store, action.payload)
       } catch (err) {
         console.error(err)
       }
      }
    }

    const commands = new Object()
    for(const [name, func] of this._selectors){
      commands[name] = func
    }

    const ret = { commands, reducer}
    return ret
  }

}

this is how I'm using it

type mystoretype ={
  coins: number
}

const coiner = new StoreBuilder<mystoretype>()

coiner.add<number>('addCoin', (s,x)=>{
  s.coins += x
})

coiner.add<number>('takeCoin', (s,x)=>{
  if(s.coins>x){
    s.coins-= x
  }
})

const {commands, reducer} = coiner.build()

but when I do this, works in javascript but gives error in typescript:

const cmd1 = commands.addCoin(23) //Property 'addCoin' does not exist on type 'Object'.ts(2339)
const cmd2 = commands.takeCoin(34) //Property 'takeCoin' does not exist on type 'Object'.ts(2339)

Upvotes: 2

Views: 549

Answers (1)

Stanislas
Stanislas

Reputation: 2020

I didn't know the answer myself, but I was intrigued to find a solution.

I don't think you can use your current syntax to achieve this result.
However, if you'd be open to modifying the way the builder is created to a Fluent Interface using Method chaining, then the following might be an option.

Note: I've simplified your example and removed the actual implementation to increase readability of the core idea. Hopefully this helps to get the idea accross.

interface Builder<TStore, TType = {}> {
  add<TName extends string, TArg>(key: TName, action: (store: TStore, param: TArg) => void): Builder<TStore, TType & { [key in TName]: (param: TArg) => void }>;
  build: () => TType;
}

// Example - Creation and usage
type mystoretype ={
  coins: number
};

const builder = {} as Builder<mystoretype>; // Using your existing implementation
const instance = builder
  .add("addCoin", (s, x: number) => { s.coins += x })
  .add("takeCoin", (s, x: string) => {})
  .build();

instance.addCoin(12);
instance.takeCoin("Abc");

// Example of errors
instance.addCoin("Abc"); // Error: as a string is not a number
instance.takeCoin(12); // Error: as a number is not a string

Typescript sandbox

Upvotes: 3

Related Questions