Home > Back-end >  Correctly type a return type based on object containing typed functions
Correctly type a return type based on object containing typed functions

Time:12-27

I am trying to write a function that receives a some argument and a map of functions from that argument type to various return types. The return type should be an object with the same keys as a the function map, but the values should be the results of applying the functions to the argument.

So basically it would be used as follows:

interface Person {
    name: string;
    age: number;
    friends: number;
}

const bunchOfPeople: Person[] = [
    { name: "Tom", age: 6 , friends: 2 },
    { name: "Dick", age: 16, friends: 12 },
    { name: "Harry", age: 26, friends: 5 },
];

const stats = applyFuncs(bunchOfPeople, {
    count: people => people.length,
    avgFriends: people => people.map(p => p.friends).reduce((a, b) => a   b) / people.length,
    allowedToDrink: people => people.filter(p => p.age >= 21).map(p => p.name),
});

// Expected stats:
// {
//   "count": 3,
//   "avgFriends": 6.333333333333333,
//   "allowedToDrink": ["Harry"],
// } 

My first attempt at this is using the return types of the functions to infer the output value types:

function applyFuncs<T, Fs extends {[K in keyof Fs]: (arg: T) => any}>(arg: T, funcs: Fs) {
  var result = {} as {[K in keyof Fs]: ReturnType<Fs[K]>};
  for (const key of (Object.keys(funcs) as Array<keyof Fs>)) {
    result[key] = funcs[key](arg);
  }
  return result;
}

This works as expected, but for some reason the compiler cannot infer the type of the functions' argument, meaning the people variable has type any. Given the generic function signature says the values of the Fs type are functions from T to any, I would have expected people to correctly be inferred to be the same type as bunchOfPeople, i.e. Person[].

TS Playground here.

CodePudding user response:

Because TypeScript is statically and structurally-typed, objects can have excess properties beyond what has been defined in the object's type. This makes performing operations that use methods which enumerate the object's keys at runtime (like Object.keys(obj), Object.entries(obj), etc.) unsafe from a static type system perspective.

If you are aware of this caveat and accept the possibility of the bugs which might arise if you don't validate the objects before using them this way, then you can use some assertions to get the types you want:

TS Playground

type Fn<
  Params extends unknown[] = any[],
  Result = any,
> = (...params: Params) => Result;

function mapResults <
  Args extends unknown[],
  T extends Record<PropertyKey, Fn<Args>>
>(args: Args, fnMap: T): { [K in keyof T]: ReturnType<T[K]> } {
  const results = {} as { [K in keyof T]: ReturnType<T[K]> };
  for (const [name, fn] of Object.entries(fnMap)) {
    results[name as keyof T] = fn(...args);
  }
  return results;
}

interface Person {
  age: number;
  friends: number;
  name: string;
}

const people: Person[] = [
  { name: "Tom", age: 6 , friends: 2 },
  { name: "Dick", age: 16, friends: 12 },
  { name: "Harry", age: 26, friends: 5 },
];

const stats = mapResults([people], {
  count: people => people.length,
  avgFriends: people => people.map(p => p.friends).reduce((a, b) => a   b) / people.length,
  allowedToDrink: people => people.filter(p => p.age >= 21).map(p => p.name),
});

console.log(stats);
  • Related