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.
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>;