Home > OS >  what is the behavior of parameters of overloaded functions in typescript?
what is the behavior of parameters of overloaded functions in typescript?

Time:10-28

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.

Playground

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' }});
  • Related