Home > Enterprise >  Typescript autocompletion for conditionally returned function based on parameter of first function
Typescript autocompletion for conditionally returned function based on parameter of first function

Time:01-25

I've been scratching my head over this for a while now, so I've decided to ask the question here on Stackoverflow in the hopes that somebody will be able to help me. This is a simplified code snippet version of my problem: TypeScript Playground

type options = "bar" | "foo";

interface barFunctions {
  barFunction: () => void;
}

interface fooFunctions {
  fooFunction: () => void;
}

interface returnFunctions {
  bar: barFunctions,
  foo: fooFunctions
}

const example = <T extends options>(options: options): returnFunctions[T] => {
  if (options === "bar") {
    return {
      barFunction() {
        console.log("It's the bar function");
      }
      // I don't expect an error here, the options parameter is set to "bar", so there is no need for a "fooFunction..."
    }
  }

  if (options === "foo") {
    return {
      fooFunction() {
        console.log("It's the foo function");
      }
      // I don't expect an error here, the options parameter is set to "foo", so there is no need for a "barFunction..."
    }
  }

  throw new Error(`Expected either foo or bar but got ${options}`)
}

To explain:

I want autocompletion on the example function once it executes with a certain option parameter.

So if I type example("foo").<autocompletion expects fooFunctions interface>. So it would show me example("foo").fooFunction() is the only option, because the argument to the first function is "foo".

And if I type example("bar").<autocompletion expects barFunctions interface>. So it would show me example("bar").barFunction() is the only option, because the argument to the first function is "bar".

However the problem now is that both return objects expect the other function to be there, even though I don't want that...

Is there any Typescript expert out there that could help me?

CodePudding user response:

There are multiple ways to solve this. Most are not type-safe (e.g. overloads, or using type assertions) as they provide no way for the compiler to validate the logic in the function implementation.

Your generic approach is not far off. First thing to fix is that your generic type T is not used to type the parameter. Let's give the parameter option the type T.

const example = <T extends options>(option: T): returnFunctions[T] => {
  return {
    get bar() {
      return {
        barFunction() {
          console.log("It's the bar function");
        },
      };
    },
    get foo() {
      return {
        fooFunction() {
          console.log("It's the foo function");
        }
      }
    }
  }[option]
};

Now to get the type-safety we want, we have to convert our function implementation to this map-shaped structure. Each key is a getter to defer the valuation of each path. We do this to avoid having to compute all paths every time the function is called. This may not matter to you if computing those is not expensive.

TypeScript can validate that this map-shaped structure conforms to the type returnFunctions[T].


Playground

CodePudding user response:

You need to actually use the type parameter as the type for options:

const example = <T extends options>(options: T): returnFunctions[T] => {
   ///...
}


example("bar").barFunction()
example("foo").fooFunction()

Playground Link

Now with this version you still have errors in the implementation because TypeScript can't really follow that if you narrow option the type of the return will also be narrowed.

You can solve this with a type assertion:


const example = <T extends options>(options: T): returnFunctions[T] => {
  if (options === "bar") {
    return {
      barFunction() {
        console.log("It's the bar function");
      }
    } as returnFunctions[T]
  }

  if (options === "foo") {
    return {
      fooFunction() {
        console.log("It's the foo function");
      }
    } as returnFunctions[T]
  }

  throw new Error(`Expected either foo or bar but got ${options}`)
}

Playground Link

Or you can instead use overloads, with the public signature being generic and the implementation signature not being generic. Although this doesn't add safety (you are responsible for making sure the parameter and return type agree, it does make the code look better IMO)

function example<T extends Options>(options: T): ReturnFunctions[T]
function example(options: Options): ReturnFunctions[Options] {
  if (options === "bar") {
    return {
      barFunction() {
        console.log("It's the bar function");
      }
    }
  }

  if (options === "foo") {
    return {
      fooFunction() {
        console.log("It's the foo function");
      }
    }
  }

  throw new Error(`Expected either foo or bar but got ${options}`)
}

Playground Link

Also I would derive options from the ReturnFunctions interface and use upper case for type (but those are just nit picks) Playground Link

CodePudding user response:

You can use strategy pattern here:

type options = "bar" | "foo";

interface barFunctions {
  barFunction: () => void;
}

interface fooFunctions {
  fooFunction: () => void;
}

interface returnFunctions {
  bar: barFunctions,
  foo: fooFunctions
}

const fnStrategy: returnFunctions = {
  bar: {
    barFunction() {
      console.log("It's the bar function");
    }
  },
  foo: {
    fooFunction() {
      console.log("It's the bar function");
    }
  }
}

const example = <T extends options>(options: T): returnFunctions[T] => fnStrategy[options]

const result = example('foo') // fooFUnctions

CodePudding user response:

You can use overloading to achieve your goal:

function example(options: 'foo'): fooFunctions;
function example(options: 'bar'): barFunctions;
function example(options: 'foo' | 'bar') : fooFunctions | barFunctions {
   // The code
}

example('foo').fooFunction(); // Autocompletion works as expected
example('bar').barFunction(); // Autocompletion works as expected
exmaple('other'); // Fails because it is not an accepted

Alternatively, you can use the keyof to maintain a syntax similar to yours:

interface returnFunctions {
  bar: barFunctions,
  foo: fooFunctions
}


const example = <T extends keyof returnFunctions>(options: T): returnFunctions[T] => {
   // The code
}

This also makes autocompletion to work as you expect.

  • Related