I am writing a library where I generate sql statements with parameters. I am hoping I can add a bit of typescript magic to make it so that when an empty list of parameters is provided, I can make that function parameter optional. I am not sure how to accomplish this generically. Here is the basic type:
class Statement<Params> {
exec(params: Params) { /* do stuff */ }
all(params: Params) { /* do stuff */ }
one(params: Params) { /* do stuff */ }
}
const create_stmt = new Statement<{ username: string; password: string }>()
create_stmt.exec({ username: 'bob', password: 'secret' })
const list_stmt = new Statement<{}>()
// What I want is to skip this argument since I know its just an empty object. As expected though, there is a type error: "An argument for 'params' was not provided."
const rows = list_stmt.all()
obviously this function would expect {}
when calling .all
. My next thought was that an undefined
arg could be skipped:
type OptionalOnEmpty<T> = keyof T extends never ? T | undefined : T
class Statement<Params> {
exec(params: OptionalOnEmpty<Params>) { /* do stuff */ }
all(params: OptionalOnEmpty<Params>) { /* do stuff */ }
one(params: OptionalOnEmpty<Params>) { /* do stuff */ }
}
const create_stmt = new Statement<{ username: string; password: string }>()
create_stmt.exec({ username: 'bob', password: 'secret' })
const list_stmt = new Statement<{}>()
// this still fails, I have to at the very least, pass list_stmt.all(undefined)
const rows = list_stmt.all()
I am hoping someone here has an idea on how I can make this work. Perhaps there is some typescript magic that I can do using tuples? In reality, building out this statement is much more elaborate, I have simplified here to show the problem.
CodePudding user response:
Something you missed was wrapping the extends
into tuples so that it isn't distributive:
type Perhaps<T> = [keyof T] extends [never] ? [] : [T];
Also, notice that I am now returning tuples instead of T
or T | undefined
. That's because now, instead of directly giving the parameter a type, we're going to be using Perhaps
as the type of all the parameters the function accepts (as rest parameters):
class Statement<Params> {
exec(...[params]: Perhaps<Params>) { /* do stuff */ }
all(...[params]: Perhaps<Params>) { /* do stuff */ }
one(...[params]: Perhaps<Params>) { /* do stuff */ }
}
When Perhaps
gives us an empty tuple, then the method accepts no arguments. When it gives us [T]
, then the type of params
is T
.
So now, when Params
is {}
, this works:
const rows = list_stmt.all(); // okay