What to do with Typescript Function Overload Growth with Optional Variables

Viewed 73

I'm having a hard time looking for a solution to deal with the exponential growth of function overloads in typescript, here's the problem:

Say I have a findOne function that gets an id, looks in the database, and finds a Person:

findOne(id):Promise<Person| undefined> {
    return query().findById(id);
}

Now I can have a variable added to throw if Person is not found (id does not exist) and make sure that findOne is not returning undefined if the variable is passed;

findOne(id):Promise<Person|undefined>;
findOne(id,orFail:true):Promise<Person>;
findOne(id,orFail?:true):Promise<Person|undefined> {
    if (orFail){
        return query().findById(id).throwIfNotFound();
    }
    return query().findById(id);
}

Now, what happens if I have another variable that decides if a field of person should be joined or not? yes, it multiplies the return types by 2 (adding another possibility for each previous one to have a joined field or not)

Again, If I have more variables, each one will multiply it by 2, so I think I will have 2^optionalVarialesCount overloads of the findOne function.

Is there a better way to do this? I don't want to overload 16 times for 4 optional variables, It really makes the code messy.

1 Answers

First of all let's define optional props as a tuple:

type Optional = [onFail: true, onPass: false]

You can define any number of props you want in Optional tuple.

Now we can overload our function:

type Person = {
  tag: 'Person'
}

type Optional = [onFail: true, onPass: false]

function findOne<
  /**
   * Infer rest arguments
   */
  Rest extends Partial<Optional>,
  /**
   * 1) if length of Rest arguments extends length of Optional arguments
   * it means that all Optional arguments was provided, which in tirn means that
   * we should return Promise<Person>
   * 
   * 2) otherwise, if Rest length is less then Optional length, it means
   * that no all Optional arguments was passes, which in turn means
   * that we should return Promise<Person | undefined>
   */
  Return extends {
    all_passed: Promise<Person>,
    not_all_passed: Promise<Person | undefined>,
  }[
  Rest['length'] extends Optional['length'] ? 'all_passed' : 'not_all_passed'
  ]
>(id: string, ...args: Rest): Return

function findOne(id: string, ...args: Partial<Optional>): Promise<Person | undefined> {
  return null as any
}

// Promise<Person | undefined> because not optional arguments are passed
const _ = findOne('id', true) 

// Promise<Person> because all optional arguments are passed
const __ = findOne('id', true, false) 

Playground

Related