Pierre
Pierre

Reputation: 6172

Restrict property decorator to specific configured type in TypeScript

Note: this question extends Property decorator only for a specific property type

--

My goal is to design a simple ORM, in which a model can be defined as such:

@model.model()
class User extends model.Model {

  @model.attr(model.StringType)
  user: string;

  @model.attr(model.StringType)
  age: number;

}

In this model, I want the compiler to throw a type error, as StringType cannot be applied to an attribute of type number. I cannot however get this to work properly -- no error is thrown.

My code so far:

interface Constructor<T> {
  new (...args: any[]): T;
}

abstract class BaseType<T> {}

class StringType extends BaseType<string> {}

type RecordKey = string | symbol;

export function attr<T>(type: Constructor<BaseType<T>>) {
  return <K extends RecordKey, C extends Record<K, T>>(ctor: C, key: K) => {
    console.log(ctor, key);
  }
}

Some pointers on how this code is not working would be incredibly useful here.

Upvotes: 2

Views: 938

Answers (1)

SpencerPark
SpencerPark

Reputation: 3506

Typescript is structurally typed which is the key to this whole answer. In this case it means any plain object can be a BaseType<T> because it has no fields/methods. Notice the inferred type reports {} for T in @model.attr(model.StringType) in the playground.

The following assignment is perfectly fine (and number can be anything):

abstract class BaseType<T> { }
const t: BaseType<number> = { };

To require that the generic is specified try:

function attr<T = never>(type: Constructor<BaseType<T>>) { ... }
// ...
  @model.attr<string>(model.StringType)
  user: string

but note that it will allow @model.attr<number>(model.StringType) because again, any object is a BaseType<number>.

If your BaseType interface is not yet complete, keep filling it out and you might find that there is enough for typescript to latch on to and infer from. For example (completely arbitrary but so you get the idea and can test the inference):

abstract class BaseType<T> {
  deserialize: (s: string) => T;
}

Upvotes: 1

Related Questions