I'm trying to add a method overload signature in an interface from an npm package that defines several specific signatures and then a catch-all signature, but my overload doesn't get picked up when I write a call that should match it; the catch-all gets picked up instead. It seems to be order-related (though of course I could be wrong), as though the catch-all is getting checked against the call before my new overload is and since it matches, is being used. How can I get my overload to be picked up?
- I've ensured that I'm extending the right interface.
- I've "proven" the catch-all is the problem by commenting it out in the original module's types — when I do that, my added overload gets picked up where it didn't before. (And then if I also comment out my overload, the call shows as an error because nothing matches.)
- I've checked that I'm not hitting the max number of overloads. There are only six originals, plus my addition would be seven; a quick check suggests the limit is much higher than that.
- I've checked that if I set aside my
.d.tsfile for a moment and add my new overload directly to the original interface, above the catch-all, it works. (Thanks for that suggestion, Eldar) - I've tried referencing my
.d.tsfile explicitly intsconfig.json(though I suspect that doesn't make sense). - I've tried referencing my
.d.tsfile via a triple-slash reference directive (though I suspect that doesn't make sense). - I've tried both of the above with a simple
.tsinstead (though I suspect... ). - I've looked at various questions about augmenting interfaces (1, 2, 3, 4, and others), but couldn't figure it out based on them.
This is meant to be a general question, but a specific example helps focus the question and possible answers: The specific case is lodash's extend method.
In @types/lodash/common/object.d.ts, extend is defined like this:
declare module "../index" {
// ...lots of other stuff...
interface LoDashStatic {
/**
* @see _.extend
*/
extend<TObject, TSource>(object: TObject, source: TSource): TObject & TSource;
/**
* @see _.extend
*/
extend<TObject, TSource1, TSource2>(object: TObject, source1: TSource1, source2: TSource2): TObject & TSource1 & TSource2;
/**
* @see _.extend
*/
extend<TObject, TSource1, TSource2, TSource3>(object: TObject, source1: TSource1, source2: TSource2, source3: TSource3): TObject & TSource1 & TSource2 & TSource3;
/**
* @see _.extend
*/
extend<TObject, TSource1, TSource2, TSource3, TSource4>(object: TObject, source1: TSource1, source2: TSource2, source3: TSource3, source4: TSource4): TObject & TSource1 & TSource2 & TSource3 & TSource4;
/**
* @see _.extend
*/
extend<TObject>(object: TObject): TObject;
/**
* @see _.extend
*/
extend<TResult>(object: any, ...otherArgs: any[]): TResult;
}
// ...lots of other stuff...
}
As you can see, there are various overloads with specific numbers of source objects, and then a "catch-all" at the very end using rest syntax.
Through machinations in @types/lodash/index.d.ts that I frankly don't understand, apparently that LoDashStatic interface ends up in the namespace _, which is where we see it when using it.
I wanted to add an overload that accepts five source objects rather than topping out at four (as an example for my answer to this question. So in a lodash-extensions.d.ts file, I did this:
declare namespace _ {
export interface LoDashStatic {
tjc(): number; // ** I'll explain this below
/**
* @see _.extend
*/
extend<TObject, TSource1, TSource2, TSource3, TSource4, TSource5>(
object: TObject,
source1: TSource1,
source2: TSource2,
source3: TSource3,
source4: TSource4,
source5: TSource5
): TObject & TSource1 & TSource2 & TSource3 & TSource4 & TSource5;
}
}
Then in test.ts, I have:
import { extend, tjc } from "lodash";
const n = tjc();
console.log(n);
const fourSource = extend({ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }, { e: 5 });
console.log(fourSource);
const fiveSource = extend({ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }, { e: 5 }, { f: 6 });
console.log(fiveSource);
The tjc there and in my lodash-extensions.d.ts is just to check that I'm augmenting the right interface. It would appear so, since TypeScript is happy with the import and the type of n is number. (Naturally, if I actually ran this code, that wouldn't work — I haven't implemented tjc anywhere.)
In the above, the type of fourSource is what you'd expect, it's picking up the four-source overload from lodash's types and so fourSource's type is a union of those source object types (and the {} target). The type of fiveSource is unknown, though, because (apparently) it picks up the catch-all extend at the end of the original module's definition, not my added overload, and since I haven't provided a type argument for TResult it's had to punt on the inference.
If I go into @types/lodash/common/object.d.ts and comment out the catch-all, my new overload gets picked up just fine. This is my "proof" (such as it is) that the catch-all is getting in the way.
I can't think of a way of setting this up in the TypeScript playground (or, of course, on-site Stack Snippets), but I've done a stackblitz with the code above, or of course if you create a project with TypeScript, install lodash and @types/lodash, and then drop the lodash-extensions.d.ts and test.ts files above into it, it should replicate the problem locally.
I can think of a number of workarounds, like declaring my own extends that is really just a reference to lodash's and specifying the overloads. But is it possible to add my overload so it gets picked up when using extends, and if so, how?