1

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)

5
  • Is your edit saying that you can do new Child({ x: 3 }).x? Commented Mar 29, 2016 at 23:28
  • Jep, this tests work where I do exactly that: gist.github.com/rweng/ab0fa6f28b522c267ab2fe0e41c0f1a2 Commented Mar 30, 2016 at 5:50
  • That test is for runtime behaviour, it does not say much about the typescript typings. I assume you want proper typing as well? Commented Mar 30, 2016 at 8:38
  • Hmm, typescript compiles it, though. But your right, on another class I had to manually declare the properties without setting them, so that typescript knows they exist. Commented Mar 30, 2016 at 11:20
  • Yeah, the main focus of typescript is to provide design-time typing, but that does not prevent you to do things that typescript does not understand but compiles fine to working code. Commented Mar 30, 2016 at 11:31

2 Answers 2

1

You cannot define the return type of the constructor, however, you can use a static factory method to achieve what you need:

export class Immutable<T> {
    // You can protect your constructor.
    protected constructor(public data: T) {
        Object.freeze(data);

        _.each(_.keysIn(data), (key) => {
            Object.defineProperty(this, key, <PropertyDescriptor>{ get: () => this.data[key] })
        })
    }

    static create<T>(data: T): Immutable<T> & T {
        return new (<any>Immutable)(data);
    }

    // Another super useful new feature in TypeScript 2.1: keyof, refer to the usage below.
    get<K extends keyof T>(key: K): T[K] {
        return this.data[key];
    }

    // ...
}

// Usage:
var obj = Immutable.create({ x: 1 });
console.log(obj.x);
console.log(obj.get("x"));

// TypeScript will generate compilation error: 
// Argument of type '""' is not assignable to parameter of type '"x"'.
console.log(obj.get("y"));

Sign up to request clarification or add additional context in comments.

Comments

0

As you have discovered the type system doesn't allow you to say that the class itself extends T for a class. You could probably hack stuff to do it using a function. But please don't. E.g. if the property update exists on T it will conflict with your Immutable.update.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.