1

I want a function that returns an object. The said function can also take n amount of functions as args, and use them to modify the object before returning it. My problem is that after doing the change I don't get the correct typescript type.

In this example I have the main function getUser and two modifier functions modStatus, modAge. When I call getUser with both modifiers I want to get UserModStatus & UserModAge & User as a return type

type User = {
  name: string;
};

type UserModAge = {
  age: number;
};

type UserModStatus = {
  status: "single" | "married";
};

type Mod<EXTRA> = <BASE>(user: BASE) => EXTRA & BASE;

const getUser = <T>(...mods: Mod<T>[]) => {
  const user: User = {
    name: "John",
  };

  return mods.reduce((acc, mod) => mod(acc), user);
};

const modAge: Mod<UserModAge> = (user) => {
  return { ...user, age: 21 };
};

const modStatus: Mod<UserModStatus> = (user) => {
  return { ...user, status: "married" };
};

const res = getUser(modStatus, modAge); // const res: User

However the return type is just the initial "User" type.

If I use static args it works fine

const getUser = <T, V>(mod1: Mod<T>, mod2: Mod<V>) => {
  const user: User = {
    name: "John",
  };

  return mod1(mod2(user));
};

const res = getUser(modStatus, modAge); // const res: UserModStatus & UserModAge & User

but I need them to be dynamic. Is there a way to achieve that?

The desired result is to get the correct type when applying different modifiers

const res = getUser();
// const res: User 
const res = getUser(modAge);
// const res: UserModAge & User 
const res = getUser(modStatus);
// const res: UserModStatus & User 
const res = getUser(modStatus, modAge);
// const res: UserModStatus & UserModAge & User 

1 Answers1

1

Yes, you can achieve that, but it needs some advanced types (or rather, I needed them). I will only show what I added, but the complete example can be inspected in the playground:

// nothing new added to the next line, but im referring often to Mod and the EXTRA generic.
type Mod<EXTRA> = <BASE>(user: BASE) => EXTRA & BASE;
// Converts Mod<any>[] into EXTRA1 | EXTRA2 | EXTRA3...
type UnionOfReturnTypes<U extends Mod<any>[]> = ReturnType<U[number]>

// Converts EXTRA1 | EXTRA2 | EXTRA3 into EXTRA1 & EXTRA2 & EXTRA3
type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

// This takes a BASE (User in our case) and intersects it with EXTRA1 & EXTRA2 & EXTRA3.
// It also combines the types UnionOfReturnTypes and UnionToIntersection
type ModifiedResult<BASE, MODS extends Mod<any>[]>  = BASE & UnionToIntersection<UnionOfReturnTypes<MODS>>;

const getUser = <T extends Mod<any>[]>(...mods: T) => {
  const user: User = {
    name: "John",
  };

  return mods.reduce((acc, mod) => mod(acc), user) as ModifiedResult<User, T>;
};


const res = getUser(modStatus, modAge); // const res: User
// No compiler errors and intellisense
res.age; 
res.name;
res.status;

Explanation:

UnionOfReturnTypes: This type is using ReturnType and indexing to combine all return types from the Mod functions. But it can only extract the EXTRA types, since the BASE information is only available when an argument is actually passed, which can't happen in a type. Thus the result is EXTRA1 | EXTRA2 | EXTRA3 and so on for every Mod that was used.

UnionToIntersection this code is from an answer of jcalz, and we need this to transform our union into an intersection. The union that UnionOfReturnTypes returns won't help us much since our wanted result of user is not:

type finalUser = {name: string} | {age: number} | {status: "married" | "single"}

but the following result:

type finalUser = {
  name: string,
  age: number,
  status: "married" | "single"
};

And exactly this does UnionToIntersection for us, it converts EXTRA1 | EXTRA2 -> EXTRA1 & EXTRA2

ModifiedResult this is the type that combines UnionToIntersection and UnionOfReturnTypes and also adds the base we couldn't extract from UnionOfReturnTypes. This finalizes the solution and now lets us dynamically add mods to getUser() and get the full type returned.

Mirco S.
  • 2,510
  • 4
  • 13
  • If interested, I can also try to explain in simple words how UnionToIntersection actually works. It might be a challenge, though =) – Mirco S. Mar 17 '21 at 20:07