Evanss
Evanss

Reputation: 23593

Make return type of function depends on its arguments with TypeScript?

I have a function that returns a promise which can be either of 2 types:

async profilesAll(
    ids: readonly number[],
    profileType: ProfileType.Staff | ProfileType.Pupil,
  ): Promise<(DatabaseSchema.staffProfiles | DatabaseSchema.pupilProfiles)[]> {
   // lots of logic here
}

Elsewhere I'm calling this function. otherFunction's return type is an array of DatabaseSchema.pupilProfiles

const otherFunction = () => {
    const results = await ctx.models.room.profilesAll(
        [root.id],
        ProfileType.Pupil,
      );
    return results;
}

TypeScript throws an error as it believes the return type can be DatabaseSchema.staffProfiles or DatabaseSchema.pupilProfiles. However in reality the logic within profilesAll prevents this.

How can I remove this error? Can I make profilesAll's return type depend on the profileType argument? I looked at conditional types but I can only see examples of them extending types, not using function arguments as I need.

Upvotes: 0

Views: 178

Answers (3)

jabuj
jabuj

Reputation: 3639

You should use overloads:

async function profilesAll(ids: number[], profileType: ProfileType.Pupil): Promise<(DatabaseSchema.pupilProfiles)[]>
async function profilesAll(ids: number[], profileType: ProfileType.Staff): Promise<(DatabaseSchema.staffProfiles)[]>
async function profilesAll(
  ids: readonly number[],
  profileType: ProfileType.Staff | ProfileType.Pupil,
): Promise<DatabaseSchema.staffProfiles[]> | Promise<DatabaseSchema.pupilProfiles[]> {
   // lots of logic here
}

Upvotes: 1

VLAZ
VLAZ

Reputation: 29007

You can use function/method overloading for this declaration.

Method

class Foo {
  //option 1:
  async profilesAll(ids: readonly number[], profileType: ProfileType.Staff): Promise<DatabaseSchema.staffProfiles[]>; 
  //option 2:
  async profilesAll(ids: readonly number[], profileType: ProfileType.Pupil): Promise<DatabaseSchema.pupilProfiles[]>; 
  //implementation:
  async profilesAll(ids: readonly number[], profileType: ProfileType.Staff | ProfileType.Pupil): Promise<(DatabaseSchema.staffProfiles | DatabaseSchema.pupilProfiles)[]> {
      if (profileType === ProfileType.Staff) return getStaff(ids);
      
      return getPupils(ids);
  }
}

Note that you need the overloaded signatures first, then and the third implementation signature should be a merge of them. See more on overloading here.

This will then allow you to make type safe calls:

declare const instance: Foo;
//OK:
const pupils = await instance.profilesAll([1, 2, 3], ProfileType.Pupil);
//OK:
const staff  = await instance.profilesAll([1, 2, 3], ProfileType.Staff);
//Error - return type is not pupilProfiles:
const wrong: DatabaseSchema.pupilProfiles[]  = await instance.profilesAll([1, 2, 3], ProfileType.Staff);

Playground Link

Function

Using functions and overloading them, is analogous but you wouldn't have a class in that case:

//option 1:
async function profilesAll(ids: readonly number[], profileType: ProfileType.Staff): Promise<DatabaseSchema.staffProfiles[]>;
//option 2:
async function profilesAll(ids: readonly number[], profileType: ProfileType.Pupil): Promise<DatabaseSchema.pupilProfiles[]>;
//implementation:
async function profilesAll(ids: readonly number[], profileType: ProfileType.Staff | ProfileType.Pupil): Promise<(DatabaseSchema.staffProfiles | DatabaseSchema.pupilProfiles)[]> {
    if (profileType === ProfileType.Staff) return getStaff(ids);
    
    return getPupils(ids);
}

/* ... */

//OK:
const pupils = await profilesAll([1, 2, 3], ProfileType.Pupil);
//OK:
const staff  = await profilesAll([1, 2, 3], ProfileType.Staff);
//Error - return type is not pupilProfiles:
const wrong: DatabaseSchema.pupilProfiles[]  = await profilesAll([1, 2, 3], ProfileType.Staff);

Playground Link

Upvotes: 0

msmolcic
msmolcic

Reputation: 6557

I don't think you can modify the return type based on the input parameter dynamically. However, in this specific case, you can specify the return type of your otherFunction by explicitly defining it.

const otherFunction = (): DatabaseSchema.pupilProfiles => { ... }

This way your otherFunction return type is no longer DatabaseSchema.staffProfiles | DatabaseSchema.pupilProfiles but DatabaseSchema.pupilProfiles only.

Upvotes: 0

Related Questions