Reputation: 488
I'm working on a class to which the user passes in a method that takes a user-defined argument, or no argument. The class exposes a "call" method that must be called with the argument requested in the user's method, performs various operations, then calls the user's method and returns the result.
This works fine if the user's function has an argument, but it breaks if the user's function doesn't use an argument.
Here's my current code (I keep trying different variations but haven't figured out the correct one yet):
class Test<T, TResult> {
methodToRun: (data?: T) => TResult;
constructor(methodToRun: (data?: T)=>TResult){
this.methodToRun = methodToRun;
}
call(data: T) {
// do some stuff, then...
return data === undefined ? this.methodToRun(data) : this.methodToRun();
}
}
const test = new Test(function(data: string){
return 1;
});
test.call("");
test.call(); // should be a signature error (currently is)
const test2 = new Test(function(data: number){
return 1;
});
test2.call(1);
const test3 = new Test(function(data: {foo: string, bar: number}){
return 1;
});
test3.call({foo: "", bar: 1});
const test4 = new Test(function(){
return 1;
});
test4.call(); // should be allowed (currently errors "Supplied Paramaters do not match any signature of call target")
test4.call({}); // should not be allowed (currently is)
One of my attempted variations was to adjust the signature of call:
call()
call(data: T)
call(data?: T) {
However that caused both test.call() and test4.call() to be allowed. How do I get typescript to ensure that the signature of call always matches the signature of the user's function?
Upvotes: 0
Views: 699
Reputation: 6456
Here's a way to do it with new ()
:
interface OneOrNoArgFunc<T, TResult> {
(arg?: T): TResult;
}
class Test<T, TResult, TF extends OneOrNoArgFunc<T, TResult>> {
methodToRun: TF;
constructor(methodToRun: TF) {
this.methodToRun = methodToRun;
}
call = ((arg?: T): TResult => {
return this.methodToRun(arg);
}) as TF;
}
let a = new Test((foo: string) => foo + "hello");
let b = new Test(() => 33);
let c = new Test((foo: string, bar: number) => foo + bar); // ERR, cannot have more than one argument
a.call("argument"); // OK
a.call(33); // ERR, wrong type
a.call(); // ERR, need argument
b.call();
b.call("argument"); // ERR, no arguments
This does require the explicit as TF
cast, and the generics don't type very helpfully: Typescript marks a
as a: Test<{}, {}, (foo: string) => string>
, seems that the inferred generics aren't passed through the function.
You can keep the parameter and return type generic parameters by using an intersection type:
constructor(methodToRun: ((arg?: T) => TResult) & TF) {
this.methodToRun = methodToRun;
}
/* ... */
// a: Test<string, string, (foo: string) => string>
let a = new Test((foo: string) => foo + "hello");
// b: Test<{}, number, () => number>
let b = new Test(() => 33);
Upvotes: 1
Reputation: 164129
This is a bit verbose but it works with your requierments:
interface TestWithParams<T, TResult> {
call(data: T): TResult;
}
interface TestWithoutParams<T, TResult> {
call(): TResult;
}
interface TestConstructor {
new<T, TResult>(methodToRun: () => TResult): TestWithoutParams<T, TResult>;
new<T, TResult>(methodToRun: (data: T) => TResult): TestWithParams<T, TResult>;
}
class Test<T, TResult> {
methodToRun: (data?: T) => TResult;
constructor(methodToRun: (data?: T) => TResult) {
this.methodToRun = methodToRun;
}
call(data?: T) {
// do some stuff, then...
return data === undefined ? this.methodToRun(data) : this.methodToRun();
}
}
const test = new (Test as TestConstructor)(function (data: string) {
return 1;
});
test.call("");
test.call(); // error
const test2 = new (Test as TestConstructor)(function (data: number) {
return 1;
});
test2.call(1);
const test3 = new (Test as TestConstructor)(function (data: { foo: string, bar: number }) {
return 1;
});
test3.call({foo: "", bar: 1});
const test4 = new (Test as TestConstructor)(function () {
return 1;
});
test4.call(); // ok
test4.call({}); // error
The only way that I can think of of doing it without (Test as TestConstructor)
is to use a "factory method":
class Test<T, TResult> {
methodToRun: (data?: T) => TResult;
static create<T, TResult>(methodToRun: () => TResult): TestWithoutParams<T, TResult>;
static create<T, TResult>(methodToRun: (data: T) => TResult): TestWithParams<T, TResult>;
static create<T, TResult>(methodToRun: (data?: T) => TResult): Test<T, TResult> {
return new Test(methodToRun);
}
private constructor(methodToRun: (data?: T) => TResult) {
this.methodToRun = methodToRun;
}
call(data?: T) {
// do some stuff, then...
return data === undefined ? this.methodToRun(data) : this.methodToRun();
}
}
And then:
const test = Test.create(function (data: string) {
return 1;
});
...
That seems very elegant, but this introduces an additional function to Test
which will also exist in the compiled js.
The previous way is strictly typescript and doesn't result in more "bloated" js.
Upvotes: 0