dodov
dodov

Reputation: 5854

How to use class type parameter in static member?

I want to implement the following pattern. A class where subclasses should have static defaults property, an options constructor argument, and an $options instance property that holds the merged values of the former two values. All three should have the same shape. Something like this:

class Foo<T> {
  static defaults: T
  $options: T

  constructor (options: T) {
    this.$options = Object.assign({}, Foo.defaults, options)
  }
}

However, I get the following error for static defaults: T:

Static members cannot reference class type parameters.

Then, TypeScript sets the type of defaults to any. After that, if I do this:

interface BarOptions {
  length: number
  color: string
}

class Bar extends Foo<BarOptions> {
  static defaults = {
    length: 'oops' // should be number
  }
}

let bar = new Bar({
  length: 42,
  color: 'green'
})

The object in new Bar({...}) is correctly type-checked, but defaults isn't, as it's considered any. This means I can mistakenly set length to string without an error. How to avoid this?

I read about the difference between static and instance sides of classes in the docs, but I still don't know how to solve this problem. Is there a solution at all?

Edit

It appears I can do it this way, although a bit clumsy:

class Foo<T> {
  static defaults: any
  $options: T

  constructor (options: T) {
    this.$options = Object.assign({}, Foo.defaults, options)
  }
}

interface BarOptions {
  length: number
  color: string
}

class Bar extends Foo<BarOptions> {
  static defaults: BarOptions = {
    length: 12,
    color: 'black'
  }
}

let bar = new Bar({
  length: 42,
  color: 'green'
})

Is there a better way that doesn't require explicitly setting static defaults: BarOptions in Bar?

Upvotes: 1

Views: 2193

Answers (1)

zhuber
zhuber

Reputation: 5534

You can't define static properties using dynamic T signature. What you could do is define abstract class and force any subclass to implement getDefaults method:

abstract class Foo<T> {
  abstract getDefaults(): T
  $options: T

  constructor (options: T) {
    this.$options = Object.assign({}, this.getDefaults(), options)
  }
}

interface BarOptions {
  length: number
  color: string
}

class Bar extends Foo<BarOptions> {
  getDefaults(): BarOptions {
    return {
      length: 3,
      color: "red"
    };
  }
}

let bar = new Bar({
  length: 42,
  color: 'green'
})

Please see playground.

Upvotes: 2

Related Questions