lonix
lonix

Reputation: 20967

Generic repository that creates instances of generic type

I'm trying to write a generic repository in TypeScript that serializes/deserializes using localStorage.

I've read many related questions regarding new() in ts, but they are full of foos, bars and bazes, and I can't find a real example. Most importantly, I can't find an example that can create a new instance inside a generic class (all examples I found assume the type is known, whereas as you'll see below the type is unknown in the repository class).

A "Dog" entity:

interface IEntity { }

class Dog implements IEntity {

  constructor(json: string);     // called when deserializing
  constructor(name: string, age: number);
  constructor(jsonOrName: string, age?: number) { /* implementation... */ }

  name: string;
  age: number;

  toJSON() {    // called when serializing (via JSON.stringify)
    //...
  }
}

And a repository to serialize/deserialize to/from localStorage.

class Repository<T extends IEntity> {

  constructor(private key: string) { }

  read(): T | null {
    const s = localStorage.getItem(this.key);
    if (!s) return null;
    const value = JSON.parse(s);
    return new T(value);            // <----------- how do I do this?
  }

  write(value: T): void {
    localStorage.setItem(this.key, JSON.stringify(value));
  }

}

The intended use is:

const dogRepository = new Repository<Dog>("dog");
const dog = dogRepository.read();
if (dog) console.log(dog.name);

Upvotes: 0

Views: 114

Answers (1)

jcalz
jcalz

Reputation: 329598

The type system is completely erased at runtime, so the type named T will not exist as anything you can construct via the new operator. Instead you need your Respository<T> instances to hold an actual runtime constructor for your T. For example:

class Repository<T extends IEntity> {

  // take a key *and* a constructor which operates on a json string
  constructor(private key: string, private ctor: new (json: string) => T) {}

  read(): T | null {
    const s = localStorage.getItem(this.key);
    if (!s) return null;
    return new this.ctor(s); // use the ctor on the JSON (don't parse, right?)
  }

  write(value: T): void {
    localStorage.setItem(this.key, JSON.stringify(value));
  }
}

And then you'd have to change this as well:

const dogRepository = new Repository("dog", Dog); // pass ctor here, T is inferred
const dog = dogRepository.read();
if (dog) console.log(dog.name);

Does that make sense? Hope that helps. Good luck!

Link to code

Upvotes: 2

Related Questions