Type Inference: How to Use Conditional Types and Generics

zirkelc

Chris Cook

Posted on March 21, 2023

Type Inference: How to Use Conditional Types and Generics

This post is a follow-up to my previous post about conditionally returning a different type 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:

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 {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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

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"});  
Enter fullscreen mode Exit fullscreen mode

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:

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

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

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:

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

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

// ...
Enter fullscreen mode Exit fullscreen mode

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. 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:

// ...
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> {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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

TypeScript playground


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 or GitHub. Thanks for reading!

💖 💪 🙅 🚩
zirkelc
Chris Cook

Posted on March 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related