Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

How to make a typescript utility type representing only the functions of an object recursively

I have a puzzle I’m working on.

I’m a developer for a programming game called bitburner

In the game we have a big object we send to the player. This object contains functions to call but also enums and other non-function fields.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

Internally we would like to have a utility type that automatically culls out everything but functions or objects that contains functions from the big object.

Here’s a small sample of the object:

const api = {
  args: ["hello", "stackoverflow"],
  pid: 5,
  hack: (server: string): void => { console.log("hack server"); },
  stanek: {
      charge: (id: number): number => {
        console.log("charge");
        return 5;
      },
      width: 5,
      height: 5,
  },
  gang: {
    power: {
      do: () => undefined,
    },
  },
  enums: {
    factions: ["a", "b"],
  },
};

and the expected type we would like is

interface ExpectedOutput {
  hack(server: string): void;
  stanek: {
    charge(id: number): number;
  }
  gang: {
    power: {
      do(): void;
    },
  },
}

hack, stanek, charge, gang, power, and do are kept because they are functions or objects that recursively contain functions.

Does anyone have any idea? I’ve been working on the playground a bit but it’s not going well.

>Solution :

First of all, let’s create a type for any function:

type AnyFunction: (...args: any[]) => any

This type can be replaced with Function but this is my personal preference.

Next, we will need a utility type that would make the result types more readable:

type Prettify<T> = T extends infer R
  ? {
    [K in keyof R]: R[K];
  }
  : never;

The logic:

  • Using mapped types map through the object
  • Using key remapping exclude any primitives, arrays, and keep the properties that are functions or objects potentially having functions in them
  • In the values check whether the value is a function then keep it as it is. Otherwise, if it is an object, recursively check its properties.

Implementation:

type OnlyFunctions<T> = Prettify<{
  [K in keyof T as T[K] extends AnyFunction
    ? K
    : T[K] extends readonly unknown[]
    ? never
    : T[K] extends object
    ? K
    : never]: T[K] extends AnyFunction
    ? T[K]
    : T[K] extends object
    ? OnlyFunctions<T[K]>
    : never;
}>;

Testing:

// type Result = {
//   hack: (server: string) => void;
//   stanek: {
//       charge: (id: number) => number;
//   };
//   gang: {
//       power: {
//           do: () => undefined;
//       };
//   };
//   enums: {
//       foo: {};
//   };
// }
type Result = OnlyFunctions<typeof api>;

Looks like almost what we need, however, some object properties are left empty since they didn’t have any function properties. To Remove them let’s write a type that would map through the keys of an object using mapped types and exclude empty objects by using key remapping:

type RemoveEmptyFields<T> = {
  [K in keyof T as {} extends T[K] ? never : K]: T[K];
};

type OnlyFunctions<T> = Prettify<
  RemoveEmptyFields<{
    [K in keyof T as T[K] extends AnyFunction
    ? K
    : T[K] extends readonly unknown[]
    ? never
    : T[K] extends object
    ? K
    : never]: T[K] extends AnyFunction
    ? T[K]
    : T[K] extends object
    ? OnlyFunctions<T[K]>
    : never;
  }>
>;

Final testing:

// {
//   hack: (server: string) => void;
//   stanek: {
//       charge: (id: number) => number;
//   };
//   gang: {
//       power: {
//           do: () => undefined;
//       };
//   };
// }
type Result = OnlyFunctions<typeof API>;

playground

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading