Search code examples
typescriptcallbacktype-inferencereturn-typeconditional-types

Change function return type based on input tranformer function in Typescript


What I am trying to do is create a simple function that would optionally accept a transformer method as input and return:

  • either the original result (if no transformer provided)
  • or the transformed result based on the transformer's return type.

A small example to showcase what I mean is the following:

// our basic example interface
interface Person {
  name: string;
  age: number
};

// the transformer fn type
type TransformerFn = <T>(person: Person) => T;

// method options contaning the optional transformer method param
interface mapperOpts {
  paramA?: number
  transformer?: TransformerFn
}

// Conditional type for returning the result based on providing transformer in options or not
type OriginalOrTransformedPerson<T extends Partial<mapperOpts>> = T extends { transformer: TransformerFn } ? 
  ReturnType<T['transformer']> :
  Person;

// test class interface
interface PeopleGetter {
  getPeople<T extends Partial<mapperOpts>>(people: Person[], opts: T): OriginalOrTransformedPerson<T>
}

class Test implements PeopleGetter {
  getPeople(people: Person[], opts: Partial<mapperOpts>) {
    if (opts.transformer) {
      return people.map(opts.transformer);
    } else {
      return people;
    }
  }
}

const people: Person[] = [{ name: 'john', age: 20 }];

const test = new Test();
const original = test.getPeople(people); // here we should have `Person[]`
const transformedResult = test.getPeople(people, { transformer: (person: Person) => person.name }); // here I would like to have the return type of 'string[]' based on transformer method

Typescript playground here

I tried to follow the plain map implementation of Array<T> interface which properly infers the return type of the provided callback method in map but I could not get it to work. I get errors in getPeople implementation and usage.

Any ideas?


Solution

  • I tried to simplify and create a more generic version of the problem and posted it here: Typescript method return type based on optional param property function

    With the really generous help and explanation of @jcalz I was able to also create a version that works for my original post here. The thing it that Type Inference is a really complicated matter for Typescript and not everything is automatically inferred as someone would expect to. Important conclusions are:

    • Type inference does not take into account Generic Constraints
    • Type inference cannot work partially. You would either provide all the type parameters in a typed function use, or none. You cannot provide one of the type arguments and let the others be inferred.

    Having said that, here is the version that works for my original question:

    // our basic example interface
    interface Person {
      name: string;
      age: number
    };
    
    // the transformer fn type
    type TransformerFn<T = any> = (person: Person) => T;
    
    // method options contaning the optional transformer method param
    interface mapperOpts<T = any> {
      paramA?: number
      transformer?: TransformerFn<T>
    }
    
    // Conditional type for returning the result based on providing transformer in options or not
    type OriginalOrTransformedPerson<T> = T extends { transformer: TransformerFn<infer R> } ? 
      R :
      Person;
    
    // test class interface
    interface PeopleGetter {
      getPeople<T extends Partial<mapperOpts>>(people: Person[], opts: T): OriginalOrTransformedPerson<T>[]
    }
    
    class Test implements PeopleGetter {
      getPeople<T extends Partial<mapperOpts>>(people: Person[], opts?: T): OriginalOrTransformedPerson<T>[] {
        if (opts?.transformer) {
          return people.map(opts.transformer);
        } else {
          return people as OriginalOrTransformedPerson<T>[];
        }
      }
    }
    
    const people: Person[] = [{ name: 'john', age: 20 }];
    
    const test = new Test();
    const original = test.getPeople(people); // here we should have `Person[]`
    const transformedResult = test.getPeople(people, { transformer: (person: Person) => person.name }); // here I would like to have the return type of 'string[]' based on transformer method
    
    

    Typescript Playground