So I have an abstract class that implements an interface:
export default interface ResponsibilityChainHandler {
setNext(handler: ResponsibilityChainHandler): ResponsibilityChainHandler
handle<T>(request: T): Promise<T>
}
import ResponsibilityChainHandler from './responsibility-chain.handler.interface'
export default abstract class AbstractResponsibilityChainHandler
implements ResponsibilityChainHandler {
private nextHandler?: ResponsibilityChainHandler
public setNext(handler: ResponsibilityChainHandler): ResponsibilityChainHandler {
this.nextHandler = handler
return this.nextHandler
}
public async handle<T>(request: T): Promise<T> {
if (this.nextHandler) {
return this.nextHandler.handle(request)
}
return request
}
}
Now, I want to define a new class that only accepts a specific object of type GreetingHandlerRequest
(which is defined as a class).
import AbstractResponsibilityChainHandler from '../../domain/responsibility-chain.handler.abstract'
import GreetingHandlerRequest from '../../domain/greeting-handler-request.domain';
export default class WelcomeHandler extends AbstractResponsibilityChainHandler {
public async handle<GreetingHandlerRequest>(
request: GreetingHandlerRequest
): Promise<GreetingHandlerRequest> {
return super.handle(request)
}
}
However, I got the following error on the GreetingHandlerRequest
import:
'GreetingHandlerRequest' is declared but its value is never read.
And if I try to do something inside the method like request.existingAttr
it complains about:
Property 'existingAttr' does not exist on type 'GreetingHandlerRequest'.
But the class GreetingHandlerRequest
has a publicly accessible attribute called existingAttr
.
I also tried to do import type
instead, but without success. I've been stuck for a while with this. Asking some mates and googling didn't result for me. Any thoughts?
There are two distinct flavors of generics in TypeScript: generic functions and generic types.
A generic function declares its generic type parameter (or parameters) on the call signature of the function, like the handle
method here:
interface RCHGenFunc {
handle<T>(request: T): Promise<T>
}
The type parameter on a generic function (T
above) isn't specified until the function is actually called, at which point the caller specifies it (or the compiler infers it on behalf of the caller). This means that a generic function implementation must be able to deal with any possible specification of T
that the function caller wants.
A generic type declares its generic type parameter (or parameters) on the type declaration, like the RCHGenType
type here:
interface RCHGenType<T> {
handle(request: T): Promise<T>
}
The type parameter on a generic type (T
above) must be specified before you can have a value of that type. If a generic type has a method that refers to the type parameter (as in handle()
above), that type parameter is fixed as soon as the surrounding generic type is specified, before that method is called. Once you are talking about, say, an RCHGenType<GreetingHandlerRequest>
, then its handle()
method only needs to be able to handle a GreetingHandlerRequest
. And so to implement that function you do not need to deal with any possible value of T
.
The distinction can feel a bit fuzzy when you have a specific type with a generic method as opposed to a generic type with a specific method. The important part is the scope of the generic. If I take generics in the type system and make an analogy with functions in JavaScript, the difference is this (the following is JS, not necessarily TS):
const rchGenFunc = () => (T) => "Promise<" + T + ">";
const rchGenType = (T) => () => "Promise<" + T + ">";
Each of them has a parameter named T
, but they have different scopes. In rchGenFunc
I don't specify T
when I call it; instead, I call it with no parameter and then get a function back that can accept any parameter. But in rchGenType
I have to specify T
when I call it, and then get back something that only works for that particular T
.
Backing up to your problem: you are using a generic function but you should really be using a generic type instead. Even in your WelcomeHandler
, your handle()
method is generic where GreetingHandlerRequest
is just a coincidentally-named type parameter, and has nothing to do with any GreeingHandlerRequest
interface/class you may be importing (which is why you get "unused" warnings).
So let's move the type parameter out of the call signature and up into the type:
interface ResponsibilityChainHandler<T> {
setNext(handler: ResponsibilityChainHandler<T>): ResponsibilityChainHandler<T>
handle(request: T): Promise<T>
}
And that means AbstractResponsibilityChainHandler
should also be generic:
abstract class AbstractResponsibilityChainHandler<T>
implements ResponsibilityChainHandler<T> {
private nextHandler?: ResponsibilityChainHandler<T>
public setNext(handler: ResponsibilityChainHandler<T>): ResponsibilityChainHandler<T> {
this.nextHandler = handler
return this.nextHandler
}
public async handle(request: T): Promise<T> {
if (this.nextHandler) {
return this.nextHandler.handle(request)
}
return request
}
}
Finally, we can talk about WelcomeHandler
. Assuming that GreetingHandlerRequest
looks like this:
interface GreetingHandlerRequest {
existingAttr: string;
}
then WeclomeHandler
can look like this:
class WelcomeHandler extends AbstractResponsibilityChainHandler<GreetingHandlerRequest> {
public async handle(request: GreetingHandlerRequest): Promise<GreetingHandlerRequest> {
request.existingAttr.toUpperCase();
return super.handle(request)
}
}
Here, WelcomeHandler
is not itself a generic class. It is a specific class that you get by extending AbstractResponsibilityChainHandler<GreetingHandlerRequest>
, which is a specific type you get by plugging in the specific GreetingHandlerRequest
type as the type parameter T
in AbstractResponsbilityChainHandler<T>
.
Now the WelcomeHandler
's handle()
method accepts only GreetingHandlerRequest
parameters. You don't have to make it generic (there's no type parameter on its declaration), and inside the implementation you have access to the specific properties of GreetingHandlerRequest
because of this restriction.
If you had said that you really do want handle()
to be a generic method and deal with any possible request type, then we'd have had to either abandon the class hierarchy (since a WelcomeHandler
would not properly handle()
all requests), or we'd have to make it accept all requests but put some kind of runtime check inside so that it does nothing unless it gets the right type of request. But that's not what you want (thank goodness) so the above solution is probably the right one for you.