Search code examples
typescripttypesdecorator

How do I force the same type on a Typescript decorator factory argument as the property it is applied to?


How can I make sure the argument type for the decorator factory matches the type of the property it is applied to?

class MyClass {
    @myDecorator("a string")
    public someProp?: string; // okay

    @myDecorator(4)
    public someProp?: number; // okay

    @myDecorator()
    public someProp?: number; // okay

    @myDecorator("a string")
    public someProp?: boolean; // error
}

I found a solution that works for specific types but I would like it to work with any property type. I can check on runtime but that doesn't seem very user friendly.

Does anyone know if this is possible?


Solution

  • Yes, it's possible. You need to make the decorator factory generic in its argument, and make sure that the target and property you decorate has a compatible value type. Here's one way to do it:

    type KeysMatchingForWrites<T, V> = { [K in keyof T]-?: V extends T[K] ? K : never }[keyof T]
    
    const myDecorator = <V = any>(val?: V) =>
      <T>(target: T, prop: KeysMatchingForWrites<T, V>) => {
        // impl here
        if (typeof val !== "undefined") {
          // you will need an assertion to assign anything
          target[prop] = val as any as T[KeysMatchingForWrites<T, V>];  
        }
      }
    

    The type KeysMatchingForWrites<T, V> returns all the keys of T whose properties can accept a value of type V written to them. So if T is {narrow: "", normal: string, wide: unknown} and V is string, then KeysMatching<T, V> is "normal" | "wide".

    Then myDecorator() accepts a value of type V, and returns a decorator whose target is some type T, and whose property key is of type KeysMatchingForWrites<T, V>. Note that inside the implementation of myDecorator() you might have to use a type assertion to actually do an assignment or the equivalent. The typing above is mostly to allow people to call the decorator properly, not to implement the decorator properly. And I also made the value optional and the V type default to any to support the case where you don't pass an argument to the decorator factory. Whether the specific typing and implementation above suits your needs has to do with your use cases; my main point here is to show that it can indeed be done.

    Let's see it in action:

    class MyClass {
      @myDecorator("a string")
      public someProp?: string; // okay
    
      @myDecorator(4)
      public someProp1?: number; // okay
    
      @myDecorator()
      public someProp2?: number; // okay
    
      @myDecorator("a string")
      public someProp3?: boolean; // error
      // Argument of type '"someProp3"' is not assignable to parameter of type '"someProp"'
    }
    

    Looks good. The last one is an error because @myDecorator("a string") is expected to be applied to a property of type string, and the only one of those in MyClass is the property named someProp. Since someProp3 is not someProp, you get the above error. Admittedly it would be nicer if the error message said "boolean is not assignable to string" instead, but at least it's an error in the right place.


    Okay, hope that helps; good luck!

    Link to code