I am facing a challenge with defining the function type for running helper functions that prepare database queries. Some of these functions have arguments, while others do not.
type PreparedQuery = {
query: string;
params?: string[] | null
}
interface Queries {
foo: {
returns: {
col1: string;
col2: string;
col3: number | null;
}
args: {
arg1: string;
}
}
bar: {
returns: Database['public']['Tables']['table_b']
args: null;
}
}
const foo = ({arg1} : Queries['foo']['args']) : PreparedQuery => {
//do stuff
return {query: "SELECT ...........", params: ['xyz']}
}
const bar = () : PreparedQuery => {
return {query: "SELECT ......"}
}
const QueryFunctions = {
foo,
bar
}
interface Database {
public: {
Tables: {
table_a: {
id: string;
name: string;
}
table_b: {
id: number;
status: 0 | 1;
create_time: Date;
update_time: Date;
}
}
}
}
class DBClient {
pool: Pool // PGPool
constructor() {
this.pool = new Pool();
}
closePool() { this.pool.end() }
async executeQuery({ query, params }: PreparedQuery) {
const res = params ? await this.pool.query(query, params) : await this.pool.query(query);
return res.rows
}
getQuery<Q extends keyof Queries>(queryName : Q, args : Queries[Q]['args']) : Queries[Q]['returns']
getQuery<Q extends keyof Queries>(queryName : Q) : Queries[Q]['returns']
async getQuery(queryName : keyof Queries, args? : Queries[typeof queryName]['args'] ){
let f : PreparedQuery = args ? QueryFunctions[queryName](args) : QueryFunctions[queryName]();
return await this.executeQuery(f)
}
}
The issue arises around the getQuery
signature. The purpose of this function is to easily extend the number of functions and ensure TS autocomplete and safety by allowing only calling functions that exist and require the correct arguments (if any).
The problem I'm encountering is related to functions without arguments like bar
. When calling getQuery('bar')
without arguments, TS complains that QueryFunctions[queryName]()
expects an argument, even though it shouldn't. On the other hand, when calling getQuery('foo')
which should require an argument, there is no complaint from TS.
Simplification Update
interface Queries {
foo: {
returns: {
col1: string;
col2: string;
col3: number | null;
}
args: {
arg1: string;
}
}
bar: {
returns: string
args: null;
}
}
const foo = ({ arg1 }: Queries['foo']['args']) => {
return { query: "SELECT col1, col2, col3 from ", params: [arg1] }
}
const bar = () => {
return { query: "SELECT * FROM table_b" }
}
const QueryFunctions = {foo,bar}
type QueryName = keyof Queries
type Args<T extends QueryName> = Queries[T]["args"] extends null ? [T] : [T, Queries[T]["args"]]
function query<T extends QueryName>(...args: Args<T>): Queries[T]["returns"] {
const [fname, fargs] = args;
const preparedQuery = fargs ? QueryFunctions[fname](fargs) : QueryFunctions[fname]() // this is causing a TS error but not a functional error
console.log(preparedQuery)
// Here you would return the real value, I cast to not get type error.
return 123 as unknown as Queries[T]["returns"];
}
query("foo", { arg1: "123" })
query("foo") // correctly complaining about missing argument
query("bar")
I have simplified Felix's original question to emphasize the complexity of handling the invocation of helper functions with or without arguments. Specifically, foo
necessitates an argument object, prompting a valid complaint from TypeScript when calling query("foo")
without the necessary argument. Conversely, bar
, lacking arguments altogether, does not prompt any errors when called with query("bar")
.
To resolve this dilemma, I attempted the following solution:
const [fname, fargs] = args;
const preparedQuery = fargs ? QueryFunctions[fname](fargs) : QueryFunctions[fname]()
While this method runs smoothly, TypeScript still raises an error, expecting an argument for QueryFunctions[fname]()
, despite the absence of arguments in bar
. How can I address this TypeScript error effectively?