Using generics to decorate a class

We previously explained how to add new attributes to a class using a decorator and properly use the decorated class accordingly to the type system. Here we use a property which type is a generic and a method which argument is a generic.

Add a property

Here we add a property named data which is an instance of some Data class.

class Data {
    name: string = ''
}
interface IFoo<D extends Data> {
  data?: D
}
function decorate<
  T extends { new (...args: any[]): {} },
  D extends Data
>(constructor: T) {
  return class extends constructor implements IFoo<D> {
    data?: D
  }
}
@decorate
class Foo {
  static new<D extends Data>() {
    return new Foo() as TFoo<D>
  }
}
type TFoo<D extends Data> = Foo & IFoo<D>

Here is how one can use it

var foo = Foo.new()
console.log(foo.data)
foo.data = {
  name: 'blabla'
}
console.log(foo.data.name)

We can also decorate using a strict subclass of data

class XData extends Data {
    x = true
}
@decorate
class Bar {
  static new<D extends XData>() {
    return new Bar() as TBar<D>
  }
}
type TBar<D extends XData> = Bar & IFoo<D>

Here is how one can use it

var bar = Bar.new()
bar.data = {
  name: 'blabla',
  x: true
}
console.log(bar.data.name)

If we want Foo to also accept Data subclasses, we must add any string signature to Data:

class Data {
    [$: string]: any
    name: string = ''
}

Here is how one can use it

foo.data = {
  name: 'blabla',
  x: true
}
console.log(foo.data.name)

Add a method

Here we add a method named test which sole argument is an instance of some Data.

class Data {
    [$: string]: any
    name: string = 'foo'
}
interface IFoo<D extends Data> {
  test(data: D): void
}
function decorate<
  T extends { new (...args: any[]): {} },
  D extends Data
>(constructor: T) {
  class U extends constructor implements IFoo<D> {
    test(data: D) {
      return data.name
    }
  }
  return U
}
@decorate
class Foo {
  static new<D extends Data>() {
    return new Foo() as TFoo<D>
  }
}
type TFoo<D extends Data> = Foo & IFoo<D>

Here is how we can use it

var foo = Foo.new()
console.log(foo.test(new Data()))

Subclassing the Data class

class XData extends Data {
    x = 421
}
@decorate
class Bar {
  static new<D extends XData>() {
    return new Bar() as TBar<D>
  }
  constructor(name: string) {
    super()
    this.name = name
  }
}
type TBar<D extends XData> = Bar & IFoo<D>

and some usage

var bar = Bar.new()
console.log(bar.test(new XData('bar')))

console.log(foo.test(new XData('baz')))

Playgrounds

Typescript playground to add a property.
Typescript playground to add a method.