0

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.

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

2 Comments

oh snap! This is exactly what I was looking for. Thank you! One question, is there a reason to prefer [keyof T] extends [never] over keyof T extends never?
@andykais It's to prevent distribution of the conditional. Try using something that isn't {} for the generic and see what happens without it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.