Home > Mobile >  Typescript - Conditionally optional generic function parameter
Typescript - Conditionally optional generic function parameter

Time:11-18

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

Playground

  • Related