Type Inference: How to Use Conditional Types and Generics
Tags: typescript, tutorial, webdev, javascript
Published: 2023-03-21 12:00:00

This post is a follow-up to my previous post about [conditionally returning a different type](https://dev.to/zirkelc/how-to-return-different-types-from-functions-in-typescript-2a2h) from a function in TypeScript. In that post, we looked at how to use conditional types to return a different type based on a condition. In this post, I'd like to dive a little deeper into generics with a code example that might be useful for everyday use.

Let's consider the following example. We have a function that serializes an object into different formats depending on the format we pass as a parameter: 

```ts
type JsonFormat = { type: "json" };
type BinaryFormat = { type: "binary" };
type StreamFormat = { type: "stream" };
type Format = JsonFormat | BinaryFormat | StreamFormat;

function serialize(
  obj: Record<string, unknown>,
  format: Format,
): any {
  // ...
}
```

We expect the return type for each function call to be different depending on the format:

```ts
const data = { a: 1, b: 2 };
// format is json, return type should be string
const s1 = serialize(data, { type: "json"});  
// format binary, return type should be Uint8Array  
const s2 = serialize(data, { type: "binary"});  
// format stream, return type should be ReadableStream<Uint8Array>
const s3 = serialize(data, { type: "stream"});  
```

To achieve this, we need to connect the parameter type of `format` to the actual return type of the function. First, we add a generic type parameter `TFormat` to the function signature:

```ts
// ...
type Format = JsonFormat | BinaryFormat | StreamFormat;

function serialize<TFormat extends Format>(
  obj: Record<string, unknown>,
  format: TFormat,
): any {
  // ...
}
```

The generic type parameter `TFormat` uses the `extends` keyword to restricts the type to a particular type or set of types. In this case, it ensures that only sub-types of the union `Format` can be used as input parameters. 

Next, we create the generic type `SerializeReturnType` to pick the right return type for each format:

```ts
// ...
type Format = JsonFormat | BinaryFormat | StreamFormat;

type SerializeReturnType<TFormat extends Format> = 
  TFormat extends JsonFormat 
  ? string
  : TFormat extends BinaryFormat
  ? Uint8Array
  : TFormat extends StreamFormat
  ? ReadableStream<Uint8Array>
  : never;

// ...
```

This generic type returns the right type depending on the generic type parameter `TFormat`. The type definition uses conditional types to map each possible format to its corresponding return type. The sequence of `?` and `:` characters are actually nested [ternary expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator). Therefore, you should read this type definition as an if-else-if ladder:

- if the `TFormat` is `JsonFormat`, the return type is `string`. 
- else if the `TFormat` is `BinaryFormat`, the return type is `Uint8Array`. 
- else if the `TFormat` is `StreamFormat`, the return type is `ReadableStream<Uint8Array>`. 
- else the `TFormat` is not one of these three types, the return type is `never`, meaning that the function cannot return anything.

Finally, we need to connect the generic type of the parameter `format` to the generic return type of the function: 

```ts
// ...
type Format = JsonFormat | BinaryFormat | StreamFormat;

type SerializeReturnType<TFormat extends Format> = 
  TFormat extends JsonFormat 
  ? string
  : TFormat extends BinaryFormat
  ? Uint8Array
  : TFormat extends StreamFormat
  ? ReadableStream<Uint8Array>
  : never;

function serialize<TFormat extends Format>(
  obj: Record<string, unknown>,
  format: TFormat,
): SerializeReturnType<TFormat> {
  // ...
}
```

The return type of the function is now _dependent_ on the `format` parameter. When we call the function with different formats, TypeScript is now able to statically infer the return type of the function:

![TypeScript Playground](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6cxfd7gvhx1ctdycoy52.jpg)

[<figcaption>TypeScript playground</figcaption>](https://www.typescriptlang.org/play?#code/C4TwDgpgBAUgzgewHYDEECcC2BDYUC8UA3lKJAFxQBEAVoklVAL4DcAsAFBnQBCAlkmzoQaLLgLFS4CJSoAjAUJCNWnblADKwdBGyZROPIRLdZcbbswr2XaVAPjC8ZA7wAfKP0HDXUD1p09Vxs1Ow0IdD5sABs+AC8IACUIYABXdCQAFWkAHkzfCAAPYAgkABM4ewxDAD4JTigofOrxIpLyyudUFrwGqAB+KHNIpABzPspmsTw20orPRR8evsGAVQFgAA4AQXR0bBAJpoLiucqAy1cVqGTsMuw5aIgLvRz1pC3d-ZAao6QIABuERCHAAZqkkABjYB8ZBDCJRWIJPIndrzVw1AAUfQQcholGSkIwZRywwEowANFAIQBrJAIADuSBqFL6oJ6k1crI4AEpKOFIjF4kkUuksrkprViH0+nxQVBMezpgA6dT4dXUOjIKg86UcRqNIlIcxQLVIALkiQwDQAeQAcsqyWM5SBMbiaDybAaoDo0hlTfQLWMoNhzgihQlkn7xZAUT0al7mJxZfLFT1VXZ1YR5ItlLqiH1DcgTSVigBRKEIMoRCT-BlNNoVonV9CYz2FqBGk2pDabITfCSl4BNqsRZWlZsQTHW+2O7Tkl1uvE89v6g2+sXU3v9g4hsOCpEi6PZWOS3AJvpMZNruUKpWGDOQAgaqjDSw6vXert4Icjlu1iB60yRtKxbNtEyLY08B7D4+z2XdCF-UCxwnUdp1tB0nVGRd3RXCDO2LPA3z0AD61ue5HmeCw9ExAs129cwhGATEjW0BBoiedB8w7L9kDYjiUKQABHVIIFEzEYK2HcQFXb1eI+dB2M45VIWiBA4CnWS5KYblvSYVcOw3f1iMwPdNHDQ8ozFE8IDjaYLzXK8OD6YAAAtFPrOsoDLPYMExKgAEkkABIUyige9cB1GwnM4b8oHuYBsAkEhsEoABGKk5EoAAmZgbAAeny8KeigPhKjNKkjKQKQnzgVyEFSaIwrkaAsNiwihjSiQNIPYVMQS7AqRMaRZDNKh9JYRpOEK4rpigBRvBASrRX9dQ6oapr5ugd5PngkApo4OK4FywgesRPqBqGmqZGoBalHGz0DpmiKiOozBluja6hnqxrmugciHieF5MDeXsvgOX5Do6uAAGZuosi7cEGyRTGoEyHsmqAgA)

---

I hope you found this post helpful. If you have any questions or comments, feel free to leave them below. If you'd like to connect with me, you can find me on [LinkedIn](https://www.linkedin.com/in/christian-zirkel/) or [GitHub](https://github.com/zirkelc). Thanks for reading!