Narrowing Multiple Return Types in TypeScript
Tags: typescript
Published: 2023-05-01 02:01:44

```TypeScript
// multiple return types
function getFruit({withCitrus: boolean}): Apples | Oranges;

// Why can't we write this?! We know Oranges are returned
var fruit: Oranges = getFruit({ withCitrus: true }); // error
```

Function `getFruit` fetches us a bag of Apples or Oranges. At compile time, as developers we know the function returns Oranges because we asked for them. Unfortunately, the compiler doesn't know this. To fix the error, the standard recommendation is to use a [Type Guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) to narrow the type. This requires extra and seemingly unnecessary code.

```TypeScript
// type guard distinguishes between Apples and Oranges
function isOranges(fruit: Apples | Oranges): fruit is Oranges {
  return "VitaminC" in (fruit as Oranges);
}

var fruits: Apples | Oranges = getFruit({withCitrus: true);
if (isOranges(fruits)) {
  // type guard narrowed the type to Oranges
  var oranges: Oranges = fruits; // ok, no compilation error
}
```

The thing is **we know the returned fruit type at compile time**; why should we use a type guard? Can we skip it? To do that, we need to rewrite the `getFruit` function.

In this [article](https://dev.to/maurerkrisztian/typesafe-multiple-return-types-wuith-typescript-54j9) Maurer Krisztián shows how to use a Generic to connect a function's arguments to its return type.

```TypeScript
type FruitType<T> =
  T extends { withCitrus: false } ? Apples :
  T extends { withCitrus: true | undefined } ? Oranges :
  Oranges;

function getFruit<T extends { withCitrus: boolean }>(opt?: T): FruitType<T>;

var apples:   Apples  = getFruit({withCitrus: false}); // ok
var oranges1: Oranges = getFruit({withCitrus: true});  // ok
var oranges2: Oranges = getFruit();                    // ok
```

Using Krisztián's formulation, we see at compile time how options passed to `getFruit` narrow the return type to that of the expected type. This is a great start, let's continue building upon it.

###Avoid Type Casting

When we implement the function definition returning `FruitType<T>` we find it requires an internal type cast (ugh). If we add an overload function, we can continue to narrow the return type while separating it from the implementation. Here's a [TS Playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgIIAd0BsIGdkDeAsAFDLnIID2AruAFzIg0C2ARtADSkWVVZUojAERQIAE2HIAPsmEBzMRBBTZwgJ4QsAgO7DuJAL4BuUqVCRYiFAHkocEPLyEeFanTCNm7Lq-LUBITlBBydVOUUIZX0-ZAA1YDA4FlAAYUYwKBoIAxMzEgtoeCRkAEkAMSzEm3QwYCoQfGIyCh1EgAtUxKzcAH5GNip+CAdc0xJSakawZAARAFFy1ABVABkAFQB9coAlZdKtmwAFdeQAXkJkNrBO7ppcDKyUPImSKdwZjGwIAGVk74eaEwOHwFwIfA8jAAzJw+IERGJJMhDMg4Ph3mBxhjkHZQr9-iDGLjHM4wRCGMgACywgKCEQhEn6eKJZJpR7ZZGo9END7jUhgdToFCVGiJdaCiAAHnWAD5zrFThAAB6QEDiJpXDpdTL3RjwLC4Z7IXpAgHIegK5DK1Xqy7XW46wE6lCyOjiCAwUASTkm4lOfAWlrkP14PkkAD04eQ8ygUEEpEjyHFQuQAHIviCZDj7CTcKnkMB8CAqDM0bhgPIQHA2DhkGAqHWJWmRWKJdKZamAHQJqPkZModPAvD5wtMEtc8uV6u1+uNlOpltgfvtrsACgATFD1+vSDA6Ag6g1kE4wIvpVaVcpbRUqmAaofGjLV1Ran1GOsAJSMRfL2WkZq8NimwvmAoJ2lqdxOk8sLtGiqT8HSwQ5mEsKdmhIH4C8sRiGANBQCAyDAa+nb2tqPTGtmeJ-CwZqMBm+I0SC4yGPkib9pQaKlgAblQwC2tAcZQLCbA0DMCAOMg7R8SgJbtNAVqxoIuC7vuD7HhAp63psADqWqcee1pXvgN6inetT1I+z6vv0SZfsgP5tn+JAAW4PIzERoHnOBNxkbqdbQZJcEIUEwgMihyBoZ2GHImGvA4XhBGrh5uAkRBjoUSG1G0aaOBZSCH5cvZt6-jKzGsVG5SqRZyBUFx0ACHA4gcbUeHOOgcZClAc7ODoUm1jhcCgKA8jdcguBwDAGnqCpIAHtVJ6LjpHQ2HVUANeIBmXmqxmLveFm4E+GE2Z+37FY5pUzXNR4LVpuk3Ct9VUI1VmgTZJnVOZPIfv+sRAdFZKkZB7I5IFuDwfCSF4kykXRVhQbIPF+HIElGGpT5kEZchDHZfReV4B+ZUkEAA) showing the error and the overload.

```TypeScript
interface IOptions { withCitrus?: boolean };
const DEFAULT_FRUIT_OPTS = { withCitrus: true } as const;

function getFruit<T>(opt?: T extends IOptions ? T : never): FruitType<T>;
function getFruit(opts?: IOptions)
{
  const _opts = { ...DEFAULT_FRUIT_OPTS, ...opts };
  return _opts.withCitrus ? bagOfOranges : bagOfApples;
}
```

###Allow Other Options

Adding another option does not interfere with return type narrowing.

```TypeScript
interface IOptions {
    withCitrus?: boolean,
    hasColor?: Apples['color'] | Oranges['color'],
};

var apples:   Apples  = getFruit({withCitrus: false, hasColor: "red"});   // ok
var oranges1: Oranges = getFruit({withCitrus: true, hasColor: "orange"}); // ok
var oranges2: Oranges = getFruit({hasColor: "red"});                      // ok
```

###Can We Always Narrow?

Regardless of how well we refine the TypeScript, there are still some situations when the return type cannot be narrowed. 

```TypeScript
var opts5 = { withCitrus: false };          // boolean
var apple5: Apples = getFruit(opts);        // error

var opts6: IOptions = { withCitrus: true }; // boolean
var orange6: Oranges = getFruit(opts);      // error

var opts7 = { withCitrus: false } as const; // false
var apple7: Apples = getFruit(opts);        // ok
```

When assigning an object to a variable, TypeScript generalizes the object. `opts5` gets type `{ withCitrus: boolean }` and therefore the return type is `Apples | Oranges`. This is the same result for `opts6`. For `opts7`, we use `as const` to convert the object to a [type literal](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-inference), and we once again have return type narrowed.

###Edge Case Typing

We have a couple of potential edge cases with the type definition of `FruitType<T>` that can cause some bugs: (1) single source of truth for the default option value and (2) triggering a [Distributive Conditional Type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types).

See this [TS Playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgIIAd0BsIGdkDeAvgNwCwAUKJLIigPJRwgDmehpllYAnuigDEoAV2BgAKnwgAecQD5kAXkrJk45BAAekEABN8BZAHcxACwDCYkbgD8ALmSgY0ZACFkRZDZWrkAIwB7AJxmDW0IPXx3G2RGZjZ8AB80TBx8Ox9Vdy0dfWRhPQgYUAhdZGSwERQYuNZ2DIpfFOx6n1qE8gouCgB6ACpkAFFdNmRzOFw8boFgKFwwABpkMFMUXSK4YSwwZAA3OCxhFACYZAADSqOzxxBzgBFBgVQAVQAZcQB9AQAlZ4BJT70AAK4gAygA6EwrSyXXDXAC2wnmyHhcDACFM3FW50uEGuoGW2LOQlEEik13WxRAYmAAVuEUqPHO2XCkXyhSppXKyyqXliTDqcPBlG+EAAjqIoKAWIS8CgwEYAnsDkd8GAlX4UABrCDoHYE3A8EAIZAIKAQNHsUIBTABKBgApiJkwO3+YQsXDCiiobamALu0zGFAIf1YMq6AIIYTwhmE4D4c0S2YQGPgJZGFC4P1bMrmoxSyDnEliST8a7q5DmrA8Sh05APJ5vT4-f6AkEQqEWKxI5DMXRLFbQCB+HhLM0WwtwZC4aU4af+qBIZAnHnCFZeyjibGTEN6Ry4XBHffIJOFw9+MBzvsaCZMivw+O4BxGVa3M5OFyuCkQGgPkDsVxKHjc5AmCC0QDOcEQKCEIIOPKdHTrFcLl5ZJ4CwSZIOQP5bhWYCEAmCB0xQUw4F2FA4EoO540qYA-DXYByLGOldBpOkDjUKRkAAbQARgAXSgrdgLgGBnAQMB8D9IxZRuGgQA4gAKHcWIASmWaAFKgGsKBtaA0VdYCIH2Q5LV0KC-h2UjMAiNUlXNB0oFw7EQygc0JOWLiCTw-AZwdNFaRAL07l1CJWNYfJ0DrQdkDYf8pRNXh+HySYygrdZfxKWSHOEJzPP4AdTHw0MynHS1KCneiWCgn0Vn9FhAxAAIdnmBLLyZf8kAPOBtOQF0oDjfAtDgeEWiWHAwAAcnwMiAmANKpRYNgpVYCrbkQBB5oZDjWNauiGLpBwzm40DYP4sJcnwbjXHOmJwXuyDul45AAFpkFMMAwHQJ8eh6Ix-vBJK8DNYA9SweJwTtFgegjBBcB6Ui9FArUegAJh6XdWLAQKDheoHPQ++EsAAYl22j6Ox8iXsxtiFKwPGpFwSg+h6bpd2RBsXneL5fgBD5gTBJRCGMMwYWsBxcQ8Xt8HZsBOm4LjizJfgZk0UpZAUZRGjUC6woMEXoW7WwHA-Ab3E8bxtdUE6YPA86cj1nibr5dp2GSDAWnSTI3F1tkCkpEoygqKQV05psedbfn20hUWja8b3XfSZo0mQBpVETzogA) for a deeper explanation.

### Final Form

Tying it all together.

```TypeScript
type FruitType<T> =
  T extends { withCitrus?: infer B } ?
    [boolean] extends [B] ? Oranges | Apples :
    B extends undefined | typeof DEFAULT_FRUIT_OPTS.withCitrus ?
    Oranges : Apples :
  Oranges;

function getFruit<T>(opt?: T extends IOptions ? T : never): FruitType<T>;
function getFruit(opts?: IOptions)
{
  const _opts = { ...DEFAULT_FRUIT_OPTS, ...opts };
  return _opts.withCitrus ? bagOfOranges : bagOfApples;
}
```

Find the complete code and tests for return type narrowing in this [TS Playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgIIAd0BsIGdkDeAsAFDLnIID2AruAFzIg0C2ARtADSkWVVZUojAERQIAE2HIAPsmEBzMRBBTZwgJ4QsAgO7DuJAL4BuUqVCRYiFAHkocEPLyEeFanTCNm7Lq-LUBITlBBydVOUUIZX0-ZAA1YDA4FlAAYUYwKBoIAxMzEgtoeCRkAEkbdDBgKhB8YjIKHUSAC1TErNwAfkY2Kn4IBwNeZrhcVP5BbrRMHFwAbQByAMEFgF0ZZDtQvEXlqDXc0xJSalqwZAARAFEAMVQAVQAZABUAfRuAJXvSt5sABWeAGVkABeQjIJpgVrtGi4DJZFCGZCjPhnI75U64c4YbAQQHJXH4UHIOYEPgeRgAZk4fECIjEkmQhk4ZPcDGQAFYaXsRJFokyWeT2QB2bkTIIaLS6KSGdYozFgI4Kzb2Rx4gmzYmkoWeZAAFjFdOCqrCNISSRSIHSyEy2QFrNo7IAbIbBLylCozYlkml4XbZcj8Ar0SQwOp0CgbllEs9wxAADzPAB8oNiz2QEAAHpAQOI6hCWm1bV1GKAYNBkAAhJnITqxXhzXr9BzrLM5vMkyvrToq7b4WQ4zX0esUatt5QdujiCAwUASDZhiNUGCXW4PF7vL4-V7-IEAOkh0OLtd7avww4avHIg+cF94WzPIdIMDoCCqNWQTjAUZoiUTSYACioSopnTcdc3wcpKmqWoT3TLwIAAN2gABKRgfxjON-yOF8QDfGDPwgb9ozAIDKhLMoKnfWoUNIepeGVV5gLAfAwTJPcOOuO4njeT5vl+AFARpDi92Y-A8mOS9kDEMAaCgEBkAAkdyCY8iD0LGF8DrKT7xNdUWEJOYHycfEDNmPdZ1zUpcyzIDQRTABCVSWL3EYxnFGRZCoPc9lBMFnNwVzRnGQIUPWO8r2vGZ9MMm9TMJCzQHEazp0zOyQUcgKgvcwJPOQbzfJBfyxOykLBDC2IUKOQx8kQuAoGRaLcAARkYaZCXIMEvww0iCEPIsOkYeAsFwCBDCq0g6oauAmoAJjam8iS6oieoAvqNOLIa4BGnJkDcsqJQZYRxqOKb8r0lrGGM5xluI39ev6zS-TGiaSDOkIz3m08nFYwi7sSADXvei7KSui7iW6ki1v28V3SiFQTsm+rzr7PUwb7CGVqh9aoQG2FGCnGc53ERHJNIAB6cnkCuTMNQgK6AGkCyhZBhtGxT5RqLEaSwRJoG22i3uRsTiTJR7NtZ7bRppGGjQUD1jsDVEsVO5HHj5+wsDa3hFsx-7SLEoHkesyxRvwmp0JI2MI3jKDqNwJNVYai4ZzgGgsF1K8euthNCcsiRHfySnqdpsz6epqAoEEZnmmQJscAcRTFxQYB8CcEB+awMVpwhQQAGtcEF97yOa0WY7xuFJZ2mXgthiIFaVhUJLOrNWvazVbtWsTmqqyLeGD6Ao6gJGGrAdA2+T5d8pL9Tcc04w+-7qn44GEAg6pmm6cYK5I+jw8476BOFIA5PkFTwiM817OUB0fPC5H6eWK+u2YN+sWNsGm0ESZJ2M0zL7rq-UhvdMiT9e6LwHrvYeklg6bzDozGOX87QAU5mcHmGsBYPzEpSMu4tP5s0RI3LmioH5ZlBu3G6f0u7kUpOAiBVMqB5wxFzfoe4BDyDWgAcQ9DeRgrcaTXWunw-+NIPgSF4X-SkpMgA).

In this article we explain how to narrow a function's multiple return types using its argument list; we show how to inform the compiler which return type the caller expects. We do this in a type safe manner and avoid superfluous type casting. We encourage extra steps to avoid bugs when changing types and defaults. Finally, we explore various edge cases where return type narrowing requires extra focus by using type literals or type guards.

Please comment below and let me know what you think.