Function with Conditional Return Type in TypeScript

Updated: January 7, 2024 By: Guest Contributor Post a comment

Introduction

TypeScript enhances JavaScript by adding types to the language. One advanced and highly useful feature in TypeScript is the ability to define functions with conditional return types. This feature is imperative when dealing with situations where a function’s return type depends on the parameters passed to it.

Understanding Conditional Types

Before delving into conditional return types in functions, it’s vital to understand conditional types in TypeScript. At their core, conditional types help you work with types that are determined based on conditions. They are similar to JavaScript ternary operators but applied at the type level.

type ConditionalType<T> = T extends string ? string : number;

Basic Conditional Return Types in Functions

Let’s start with a basic example of a function with a conditional return type:

function getValue<T extends boolean>(flag: T): T extends true ? string : number {
    return flag ? 'This is a string' : 42;
}

In this example, getValue can return either a string or a number based on the truthiness of the input flag.

Inferring Within Conditional Types

More advanced usage of conditional return types comes when you want TypeScript to infer nested types within generics:

type WrappedValue<T> = { value: T };

type Unwrap<T> = T extends WrappedValue<infer U> ? U : T;

function unwrapValue<T>(val: WrappedValue<T>): Unwrap<T> {
  return val.value;
}

Here, TypeScript will infer the specific type U of the value that’s wrapped inside a given object, another application of conditional types where the function reflects this dynamic.

Conditional Return Types with Overloads

Function overloads in TypeScript are another way to express a conditional return type:

function formatData(input: string): string[];
function formatData(input: number): number[];
function formatData(input: any): any {
    if (typeof input === 'string') {
        return input.split('');
    } else if (typeof input === 'number') {
        return input.toString().split('').map(Number);
    }
}

With overloads, we declare several function signatures that TypeScript will use to resolve the return type based on the provided argument type.

Using Advanced Patterns

Let’s go a step further with conditional return types in a real-world scenario:

type ResponseType<T> = T extends 'json' ? { data: object; }:
                           T extends 'text' ? string :
                           Blob;

async function fetchResource<T>(url: string, type: T): Promise<ResponseType<T>> {
    const response = await fetch(url);
    switch(type) {
        case 'json':
            return response.json();
        case 'text':
            return response.text();
        default:
            return response.blob();
    }
}

// Use the function
const jsonData = await fetchResource('https://example.com/data', 'json'); // { data: object; }
const textData = await fetchResource('https://example.com/data', 'text'); // string
const blobData = await fetchResource('https://example.com/data', 'blob'); // Blob

In the above code, fetchResource fetches a resource and returns different types of responses based on the request type, implementing a real-world use case with elegantly typed conditional logic.

Utilizing Type Guards

Type guards are a way to refine types within conditional logic:

type Fish = { swim: () => void; };
type Bird = { fly: () => void; };

function movePet(pet: Fish | Bird): pet is Fish {
    if ('swim' in pet) {
        pet.swim();
        return true;
    }
    pet.fly();
    return false;
}

Within the function movePet, a type guard helps TypeScript determine the specific instance of the pet type in the runtime.

Best Practices

When working with conditional return types, adhere to best practices such as keeping conditions readable, avoiding over-complication, and explicitly typing your functions whenever possible. Leverage compiler features like IntelliSense for development efficiency.

Conclusion

Conditional return types in TypeScript open up possibilities for highly dynamic function signatures. While they can introduce some complexity, they make TypeScript a powerful tool for type-safe coding. Mastering this feature means embracing both its power and its constraints to write robust, maintainable code.