I was messing around with typescript function overloading and found that it has a strange behavior.
In this case, the body parameter is undefined
in the first case but shows properly in the second.
/**
* static
*/
function backendFetch(
method: string,
path:string,
body?: { [key: string]: string | undefined }
):number
/**
* parametrized
*/
function backendFetch(
method: string,
path:string,
params: { [key: string]: string | undefined },
body?: { [key: string]: string | undefined }
):number
function backendFetch(
method: string,
path:string,
params?: { [key: string]: string | undefined },
body?: { [key: string]: string | undefined }
):number{
console.log('body:', body);
return 0;
}
const body = {'foo':'test'}
// static call
backendFetch('get','/create',body);
// parametrized
backendFetch('get','/{spaceId}',{'spaceId':'test'},body);
the output is:
[LOG]: "body:", undefined
[LOG]: "body:", {
"foo": "test"
}
any idea why? and how to resolve this to get the body
parameter in both cases?
CodePudding user response:
Typescript doesn't track the name of your parameters in overloads, it's the positions that matter. In fact at runtime, the overloads are removed entirely. Overloads exists only to provide more specific types for certain patterns of arguments.
Your implementation still must handle all possible argument signatures and do everything as if the overloads didn't exist at all.
That means that you implementation function must be directly callable according to how you are actually calling it.
And right now, it isn't.
So with this signature:
function backendFetch(
method: string,
path:string,
params?: { [key: string]: string | undefined },
body?: { [key: string]: string | undefined }
)
And this call:
backendFetch('get','/create', body);
Then body
is taking the spot of the third argument, what you call params
.
The only reason you don't get a type error is that params
and body
have the same type so Typescript doesn't notice that you passed a body
where it's expecting a params
.
To make this work, your implementation could look like this:
function backendFetch(
method: string,
path:string,
paramsOrBody?: { [key: string]: string | undefined },
body?: { [key: string]: string | undefined }
):number{
const realParams = body ? paramsOrBody : undefined
const realBody = body || paramsOrBody
console.log('body:', realBody);
return 0;
}
Your implementation has to support ALL overload signatures, so it check to see if there is a fourth argument and then reinterprets the meaning of those arguments in that case.
Lastly, keep in mind there are a lot of ways you could shuffle the arguments around. You could use a ...rest
parameter instead and check its length. Or you could make body
always be the third argument, and have params
be the optional fourth (in which case you you may not need overloads at all). But it's up to you where to go from here now that you know what is going on.
CodePudding user response:
as @Alex pointed out: Typescript doesn't track the name of your parameters in overloads, it's the positions that matter.
However, since it differs in only trailing parameter, I don't think it is necessary to define overload for it.
In any case, you should keep your parameters positions intact in overloads, like so:
// Don't
function f1(p1:Number, p2?:String): void;
function f1(p1:Number, p3:String, p2?:String): void;
// Do
function f1(p1:Number, p2?:String): void;
function f1(p1:Number, p2?:String, p3:String): void;
function f1(p1:Number, p2?:String, p3:String): void {
console.log({p1, p2, p3});
}
// Better yet - since it is just a trailing parameter, don't overload at all
function f1(p1:Number, p2?:String, p3?:String): void {
console.log({p1, p2, p3});
}
As a bonus, if you really don't want to care about where you put your parameters, then you can just use destructured parameters:
type DynamicKeyedObj = { [key: string]: string | undefined };
type BaseType = { method: string, path: string };
// dividing type just for the sake of it
type Overload = BaseType & { params?: DynamicKeyedObj, body?: DynamicKeyedObj };
function f1({method, path, body, params}:Overload): void {
console.log({method, path, params, body})
};
f1({method: 'post', path: '/', body: { something: 'yes' }});
f1({method: 'get', path: '/:id', params: {id: '123'}});
f1({method: 'post', path: '/:id', params: {id: '123'}, body: { something: 'yes' }});