There is an important difference between generic call signatures and generic types. A generic call signature has the generic type parameters on the call signature itself, like this:
interface TestInterfaceOrig {
doSome<T extends object>(value: T): void
}
When you have a generic call signature, the caller gets to specify the type argument:
declare const testInterfaceOrig: TestInterfaceOrig;
testInterfaceOrig.doSome<{ a: string, b: number }>({ a: "", b: 0 });
testInterfaceOrig.doSome({ a: "", b: 0 });
In the above, the caller is able to choose that T is {a: string, b: number} in both calls. In the second call, the compiler infers that type from the value argument, but it's the same... the caller is in charge. The implementer, on the other hand, has to write doSome() so that it works for any T the caller chooses:
const testInterfaceOrig: TestInterfaceOrig = {
doSome<T extends object>(value: T) {
console.log(value); // 🤷♂️ there's not much I can do,
// all I know is that value is an object
}
}
It doesn't help or change anything for the implementer to choose a type argument default, since defaults don't constrain the input in any way.
const testInterfaceOrig: TestInterfaceOrig = {
doSome<T extends object = MyOptions>(value: T) {
console.log(value); // 🤷♂️ still the same
}
}
If the implementer tries to actually constrain the input, there is an error... the caller chooses, not the implementer:
const testInterfaceOrigNope: TestInterfaceOrig = {
doSome<T extends MyOptions>(value: T) { // error!
console.log(value.foo);
}
}
This isn't what you're looking for, so you can't write it this way.
On the other hand, a generic type has the generic type parameters as part of the type declaration, like this:
interface TestInterface<T extends object> {
doSome(value: T): void
}
Here, there is no TestInterface type by itself (although you can set defaults here too, but let's not digress). You can't have a value of type TestInterface. Instead, you can have a value of type TestInterface<{a: string, b: number}>, or TestInterface<MyOptions>, or TestInterface<T> for some object type T. And once you have a value of that type, the doSome() method can only accept values of type T. The call signature is not generic; the caller is unable to choose T here. The choice of T is made by whoever supplies the TestInterface<T> instance:
class TestClass implements TestInterface<MyOptions> {
doSome(value: MyOptions): void {
value.foo
}
}
So here TestClass implements TestInterface<MyOptions>. The doSome() method only accepts MyOptions. That means the compiler knows that value has a foo property. And so the caller is not in charge of T at all, as desired:
const tc = new TestClass();
tc.doSome({ foo: "abc" }); // okay
tc.doSome({ a: "", b: 0 }); // error, that's not a MyOptions.
tc.doSome<{ a: string, b: number }>({ a: "", b: 0 }); // error, doSome is not generic
Playground link to code