Here you go:
Note: that the types of PathParams, RequestHandler, and RequestHandlerParams are invented but the point is that they are distinct and incompatible
export interface PathParams {
path: string;
params: {
id: number,
[key: string]: string | number
}
}
export type RequestHandler = (request: RequestHandlerParams) => void;
export interface RequestHandlerParams {
kind: 'query' | 'route'
value: string;
}
export default class {
use(path: PathParams, ...handlers: RequestHandler[]): this
use(path: PathParams, ...handlers: RequestHandlerParams[]): this
use(...handlers: RequestHandler[]): this
use(...handlers: RequestHandlerParams[]): this;
use(
pathOrHandlerOrHandlerParam: PathParams | RequestHandler | RequestHandlerParams,
...handlers: Array<RequestHandler | RequestHandlerParams>
): this {
// ...
return this;
}
}
Note that when creating an overload definition, the implementation signature is not available to consumers. Only the signatures that do not have an implementation are available. This is why I have added an additional signature to your example.
The reason why this works is that, in order to make the first parameter "optional" we need to give it a type that may be compatible with the array element type of either of the possible rest parameter types.
You could of course specify the parameter types in the implementation signature as any and any[] without compromising type safety for your consumers (the implementation signature is not part of the interface) but, by using a precise, well-structured union type, you can use type guards to discriminate over the arguments and verify that the implementation handles them.
This implies that you will need to determine if the first parameter is actually a PathParams via logic in the function implementation.