How to use TS generics to define a function that returns a value with a type that depends on the param type

I have four different types:

type Type1Input = {
  type1input: string;
};

type Type2Input = {
  type2input: string;
};

type Type1Output = {
  type1output: string;
};

type Type2Output = {
  type2output: string;
};

I also have a function that receives one param that can be of any of the two input types, and depending on the type of the param, the type of the return value should be one of the two output types. Something like:

type Final<T extends Type1Input | Type2Input> = (T: T) => T extends Type1Input ? Type1Output : Type2Output;

const final1: Final<Type1Input> = (obj) => ({ type1output: '' })
const final2: Final<Type2Input> = (obj) => ({ type2output: '' })

This example works, but I want only one function, so I tried:

const final = <T extends Type1Input | Type2Input>(
  obj: T
): T extends Type1Input ? Type1Output : Type2Output => {
  if (obj.hasOwnProperty("type1input"))
    return { type1output: "" };

  return { type2output: "" };
};

And even though the behaviour of the function is as expected:

// This works!
const { type1output } = final({ type1input: "" });

// This doesn't work! "Property 'type2output' does not exist on type 'Type1Output'"
const { type1output } = final({ type2input: "" });

When the function is defined it can’t understand that the return value can only be one type in each case, and shows the following errors in the returns:

Type '{ type1output: string; }' is not assignable to type 'T extends Type1Input ? Type1Output : Type2Output'

Type '{ type2output: string; }' is not assignable to type 'T extends Type1Input ? Type1Output : Type2Output'.

So, I’m looking for a way to implement this function without TypeScript complaining. Any ideas?

It looks like there is a way called function overloads:
https://www.typescriptlang.org/docs/handbook/functions.html#overloads

It is as simple as declaring two typings for that function:

TypeScript Playground

type Type1Input = {
  type1input: string;
};

type Type2Input = {
  type2input: string;
};

type Type1Output = {
  type1output: string;
};

type Type2Output = {
  type2output: string;
};

function final(obj: Type1Input): Type1Output;
function final(obj: Type2Input): Type2Output;

function final(obj) {
    if (obj.type1input) return { type1output: '' };
    else return { type2output: '' };
}

const return1 = final({ type1input: '' }) 
const return2 = final({ type2input: '' })

return1.type1output  // ✔ No Error (as expected)
return1.type2output  // ✔ Error (as expected)
return2.type1output  // ✔ Error (as expected)
return2.type2output  // ✔ No Error (as expected)
1 Like

It works! :smiley: However, it seems that in some cases, the order of the definition can be troublesome.

function mergeSettings(s: ImportedMono): NormalizedMono;
function mergeSettings(s: ImportedMulti): NormalizedMulti;

function mergeSettings({
  packages,
  ...settings
}: ImportedMulti | ImportedMono): NormalizedMulti | NormalizedMono {
  return merge(
    {
      ...defaultSettings,
      packages: packages.map(pkg =>
        merge({ active: true }, typeof pkg === "string" ? { name: pkg } : pkg)
      )
    },
    settings
  );
}

function normalizeSettings(settings: ImportedSettings): Settings {
  if (!Array.isArray(settings)) return mergeSettings(settings);
  return settings.map(s => mergeSettings(s));
}

In the code above, TS identifies the type of mergeSettings on the second call as:

function mergeSettings(s: ImportedMono<Package>): NormalizedMono<Package> (+1 overload)

when it should be:

function mergeSettings(s: ImportedMulti<Package>): NormalizedMulti<Package> (+1 overload)

So in order to fix it I had to change the order of the type definitions:

function mergeSettings(s: ImportedMulti): NormalizedMulti;
function mergeSettings(s: ImportedMono): NormalizedMono;

Maybe the last one is the default one if TS is not capable of recognizing which one it is?

By the way, the TS playground is quite nice :slight_smile:
http://www.typescriptlang.org/play/