Christoph Herrmann
Christoph Herrmann

Reputation: 11

How to define proxy functions giving params tuple to complex functions in TypeScript

I currently rewrite a lib from JavaScript to TypeScript and needs to define proxy functions. They should accept all types and amount of params give them to the origin function like this:

async function any(table: string, columns?: string[], conditions?: object): Promise<object[]>
async function any(table: string, conditions?: object): Promise<object[]>
async function any(queryFunction: object): Promise<object[]>

async function any(...params: any[]): Promise<object[]> {
    return Promise.resolve([])
}


async function one(table: string, columns?: string[], conditions?: object): Promise<object>
async function one(table: string, conditions?: object): Promise<object>
async function one(queryFunction: object): Promise<object>

async function one(...params: any[]): Promise<object> {
    return (await any(...params))[0]
}

But tsc give me the error A spread argument must either have a tuple type or be passed to a rest parameter..

The solutions for the issue I found doesn't really fit in my use case. I don't want to handle all possible param variations in the body of the proxy function, because it would be a big overhead. Also I don't want to have a ...param: any[] overloading definition in the first function, because the param variations should be strict.

So someone have an idea how to solve that kind of issue in a good way?

Thanks and Greetings.

Upvotes: 1

Views: 87

Answers (1)

jcalz
jcalz

Reputation: 328362

Overloads in TypeScript are fairly limited in usefulness:

  • You can call an overloaded function with a single statically-known call signature and that's it. In your code example, you could call any("abc", ["def"]) or any("abc", {c: 0}) or any({q: 1}), but you can't call it with anything like a union of its possible parameters. This is suggested in microsoft/TypeScript#14107, but it has not been implemented.

  • When you implement an overloaded function you generally widen the function parameters to make it easy on yourself, but then the compiler no longer keeps track of the particular ways the function could actually be called. So inside of your implementation of one(), you've got params as type any[]. The compiler doesn't know that it will actually be either [string, string[]?, object?] or [string, object?] or [object]. It's just any[]. So you can't call any(...params), because any()doesn't accept an argument list of typeany[]`. There's a suggestion to support keeping track of the call signatures inside the implementation at microsoft/TypeScript#22609, but again, this is not implemented.

These two issues together make it tough to approach things the way you're doing. If you really want to use overloads, you should probably just use a type assertion to suppress the error and move on, noting that this is not making your compilation quieter, not safer:

async function one(...params: any[]): Promise<object> {
  return (await any(...params as [string]))[0]; // no error now
}

A common use for overloads is that different call signatures can have different return types. But in your case they all return the same return type for a given function, and that suggests a different approach: write a single call signature that takes a rest parameter whose type is a union of tuples. It could look like this:

type MyParams =
  [table: string, columns?: string[], conditions?: object] |
  [table: string, conditions?: object] |
  [queryFunction: object]

async function any(...params: MyParams): Promise<object[]> {
  return Promise.resolve([])
}

async function one(...params: MyParams): Promise<object> {
  return (await any(...params))[0]
}

Here the MyParams type represents the union of possible ways of calling any() and one(). Inside the implementation of one(), the compiler knows that params is the same exact type as the type passed to any(), so it lets you call it with no error. And you can verify that calling any() and one() behaves very similarly to the overloaded version:

// 1/3 any(table: string, columns?: string[] | undefined, 
//   conditions?: object | undefined): Promise<object[]>
// 2/3 any(table: string, conditions?: object | undefined): Promise<object[]>
// 3/3 any(queryFunction: object): Promise<object[]>    
any("abc", ["def"]); // okay
any("abc", { c: 0 }); // okay
any({ q: 1 }); // okay

// 1/3 any(table: string, columns?: string[] | undefined, 
//   conditions?: object | undefined): Promise<object>
// 2/3 any(table: string, conditions?: object | undefined): Promise<object>
// 3/3 any(queryFunction: object): Promise<object>    
one("abc", ["def"]); // okay
one("abc", { c: 0 }); // okay
one({ q: 1 }); // okay

Indeed, when you look at the IntelliSense hints when you call one() or any(), you are actually shown multiple call signatures as if they were "genuine" overloads.

Playground link to code

Upvotes: 2

Related Questions