Typesafe multiple return types based on parameter value with Typescript.
Krisztián Maurer

Krisztián Maurer @maurerkrisztian

About: Hi, I'm Maurer Krisztián A passionate full-stack developer from Hungary

Location:
Hungary
Joined:
Jul 19, 2022

Typesafe multiple return types based on parameter value with Typescript.

Publish Date: Jul 21 '22
13 5

Typescipt types and runtime values are two separate levels, basically writing typescript conditions based on values is not supported, but there is a solution.

What I would like to achieve is the following: A function return type changes based on the value entered in the function parameter.

function getUsers(withPagination: boolean): IUser[] | IPaginatedUsers { 
  // ... 
  return users; 
}
Enter fullscreen mode Exit fullscreen mode

The purpose of this function is to return users from an api, if withPagination is true then it wraps the user array in pagination, if false then only the user array is returned (just an example).

The problem is that runtime reveals the return value based on the "withPagination" variable the return value is not specific, but we know what it will be, it can cause some headache.

One solution would be to specify the return value generically, but that is not very convenient and can be messed up. The other solution is to create two separate functions, which in this case might be better but it can cause code duplication.

function getUsers(): IUser[] { 
  // ... 
  return users; 
}

function getUserswithPagination(): IPaginatedUsers { 
  // ... 
  return users; 
}

Enter fullscreen mode Exit fullscreen mode

If we want to keep it together and implement 2 or more different returns in a typesafe way, then you can do it this way:

interface IUser {
    name: string,
    age: number,
}

interface IPaginated<T> {
    data: T[],
    meta: {
        page: number,
        limit: number,
        totalPages: number
    }
}

interface IResponseOptions {
    withPagination: boolean
}

const DEFAULT_RESPONSE_OPTION = {
    withPagination: true
}

type UserResultType<T extends IResponseOptions> = T extends {
    withPagination: true
} ? IPaginated<IUser> : IUser[];


function getUsers<T extends IResponseOptions>(withPagination: T = DEFAULT_RESPONSE_OPTION as any): UserResultType<T> { 
  // fetch data..
  return "something" as any as UserResultType<T>; 
}



getUsers({withPagination: true}); // return IPaginated<IUser>
getUsers({withPagination: false}); // return IUser[];
getUsers(); // returns bsed on DEFAULT_RESPONSE_OPTION: IPaginated<IUser>


Enter fullscreen mode Exit fullscreen mode

Comments 5 total

  • Andrew Philips
    Andrew PhilipsApr 24, 2023

    I like where you're going with this. One issue, your proposal doesn't quite address everything I think you'd hope for. If we assign the return value to a variable, we can see that the return type doesn't match for one of them.

    var foo: IPaginated<IUser> = getUsers({withPagination: true}); // ok
    var baz: Paginated<IUser>  = getUsers(); // error
    var qux: IPaginated<IUser> | IUser[] = getUsers(); // ok
    
    
    Enter fullscreen mode Exit fullscreen mode

    I'm not sure how to fix it.

    TS Playground

    • Krisztián Maurer
      Krisztián MaurerApr 25, 2023

      Yes, you are right. I found a solution for this:

      const DEFAULT_RESPONSE_OPTION: { withPagination: true } = {
          withPagination: true
      }
      
      Enter fullscreen mode Exit fullscreen mode

      Add an explicit interface { withPagination: true } beacaues the typeof DEFAULT_RESPONSE_OPTION is boolean without this. (idk how to get the explicit type of a constant with type inference)

      Then add typeof DEFAULT_RESPONSE_OPTION as the default value of the T generic.

      function getUsers<T extends IResponseOptions = typeof DEFAULT_RESPONSE_OPTION>(...)
      
      Enter fullscreen mode Exit fullscreen mode

      TS Playground

      • Andrew Philips
        Andrew PhilipsApr 25, 2023

        Nice. Thank you.

        I tried modifying your DEFAULT_RESPONSE_OPTION slightly with something I recently learned about Literal Types and Literal Inference, specifically the type modifier as const.

        const DEFAULT_RESPONSE_OPTION = { withPagination: true } as const;
        
        Enter fullscreen mode Exit fullscreen mode

        TS Playground

  • Krisztián Maurer
    Krisztián MaurerMay 5, 2023

    I found a cleaner way to achieve this with function overloads Ts Playground

Add comment