Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to leverage discriminated union to infer return type of a function

Given the following types, interfaces, and getData function below I'm trying to find a way to leverage discriminated unions so that the TS compiler can narrow the return type of getData(source: DOSources) to the associated DOTypes

// Expected behavior
const result = getData("dataObjectA");

// result.data should be a string but in this case the TS compiler will complain 
// that data does not have the toLowerCase() function
result.data.toLowerCase();

Example Code

interface DataObjectA {
  source: "dataObjectA";
  data: string;
}

interface DataObjectB {
  source: "dataObjectB";
  data: number;
}


type DOTypes = DataObjectA | DataObjectB
type DOSources = DOTypes["source"];

async function getData(source: DOSources) {
  const response = await fetch(`https://some-random-endpoint/`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  });

  switch (source) {
    case "dataObjectA":
      return await response.json() as DataObjectA;
    case "dataObjectB":
      return await response.json() as DataObjectB;
  }
}
like image 479
IAmNotANumber Avatar asked Jul 28 '21 00:07

IAmNotANumber


People also ask

How is a discriminated union defined?

Discriminated unions are useful for heterogeneous data; data that can have special cases, including valid and error cases; data that varies in type from one instance to another; and as an alternative for small object hierarchies. In addition, recursive discriminated unions are used to represent tree data structures.

What is tagged union in TypeScript?

TypeScript 2.0 implements a rather useful feature: tagged union types, which you might know as sum types or discriminated union types from other programming languages. A tagged union type is a union type whose member types all define a discriminant property of a literal type.


1 Answers

You can indeed get the compiler to compute the desired return type of getData() as a function of the DOTypes discriminated union and the type of the source parameter. You can make getData() a generic function whose type parameter K extends DOSources is the type of the source parameter. For example:

async function getData<K extends DOSources>(source: K) {
  const response = await fetch(`https://some-random-endpoint/`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  });

  return await response.json() as Extract<DOTypes, { source: K }>
}

To find the member of the DOTypes discriminated union associated with K, we can use the Extract utility type. Extract<DOTypes, {source: K}> selects from DOTypes all union members whose source property is of a type assignable to K.

Note that we have to assert that the function returns a value of (a Promise corresponding to) this type; the compiler is unable to verify that.


Let's test it:

const resultA = await getData("dataObjectA"); // const result: DataObjectA
resultA.data.toLowerCase();

const resultB = await getData("dataObjectB"); // const result: DataObjectB
resultB.data.toFixed();

Looks good. Each result is narrowed to the expected type. You'll only get a union out of getData() if you put a union in:

const resultAOrB = await getData(Math.random() < 0.5 ? "dataObjectA" : "dataObjectB");
// const resultAOrB: DataObjectA | DataObjectB

Playground link to code

like image 111
jcalz Avatar answered Oct 13 '22 14:10

jcalz