kolurbo
kolurbo

Reputation: 538

How to type decorators in TS

I wonder how to correctly type method decorators in Typescript. I have a simple decorator which adds string param as a first parameter to the decorated function. It's working just fine the problem is that I don't know how to type it correctly and whether it's even possible?

const task = (
  target: unknown,
  propertyKey: string,
  descriptor: TypedPropertyDescriptor<(...args: unknown[]) => unknown>,
): TypedPropertyDescriptor<(name: string, ...args: unknown[]) => unknown> => {
  const original = descriptor.value
  descriptor.value = function (...args: unknown[]) {
    return original?.call(this, 'Mark', ...args)
  }

  // what's wrong with this return?
  return descriptor
}



class Test {

    constructor(){
      // should not report an error
      this.printName()
    }


    @task
    printName(name: string) {
        console.log(name)

    }

}


  new Test()

Playground available here

Upvotes: 2

Views: 2492

Answers (2)

jperl
jperl

Reputation: 5112

As @cdimitroulas said, it's not possible, decorators don't change the Typescript type.

Still, we can address some of your questions:

// what's wrong with this return?

const task = (
  target: unknown,
  propertyKey: string,
  // you said here that descriptor is this type
  descriptor: TypedPropertyDescriptor<(...args: unknown[]) => unknown>,
  // yet, you said that the type is different here, namely that the first argument is of type string (it was unknown)
): TypedPropertyDescriptor<(name: string, ...args: unknown[]) => unknown> => {
  const original = descriptor.value
  descriptor.value = function (...args: unknown[]) {
    return original?.call(this, 'Mark', ...args)
  }

  // you're returning the same descriptor that was passed to the decorator
  return descriptor
}

Typescript expects to find the same type but it was different, so it tries to compare them to see if one is assignable to the other but it's not. unknown is not assignable to string. unknown is only assignable to unknown or any. Knowing that a decorator won't change the type anyway, you can omit the return type completely and let Typescript infer that for you

const task = (
  target: unknown,
  propertyKey: string,
  descriptor: TypedPropertyDescriptor<(...args: unknown[]) => unknown>,
) => {
  const original = descriptor.value
  descriptor.value = function (...args: unknown[]) {
    return original?.call(this, 'Mark', ...args)
  }

  return descriptor
}

Next, Typescript still isn't happy with that:

@task // --> Type '(name: string) => void' is not assignable to type '(...args: unknown[]) => unknown'
    printName(name: string) {
        console.log(name)

    }

As said earlier, unknown cannot be assigned to string. So either we change unknown to any (unsafe) or better yet, we change the signature to TypedPropertyDescriptor<(name: string) => void>

const task = (
  target: any,
  propertyKey: string,
  descriptor: TypedPropertyDescriptor<(name: string) => void>,
) => {
  const original = descriptor.value
  descriptor.value = function () {
    return original?.call(this, 'Mark')
  }

  return descriptor
}

As for this line

this.printName()

the best you can do is making the name prop optional. The decorator is basically setting the prop for us so in that case, we can remove the prop altogether although I know it's not a real case, just an example to illustrate your point.

Upvotes: 2

cdimitroulas
cdimitroulas

Reputation: 2539

It's not possible to modify a function's type signature using a decorator.

See relevant discussions here and here.

Upvotes: 1

Related Questions