As pointed out by jcalz, achieving this particular type of arbitrary transformation is not feasible in TS.
However, if you are using the function simply for navigating through the tree structure, there are more efficient methods available.
If your goal is to maintain navigational capabilities within a tree rather than as a flat object, you can leverage tools like lodash
and a utility type from type-fest
to type the return value.
import _ from "lodash"
import { Get } from "type-fest"
function mapObject<
Dict extends Record<string, any>,
Path extends string
>(
obj: Dict,
path: Path
): {
[Key in keyof Dict]: Get<Dict, `${Key}.${Path}`>
} {
const ret = {} as Record<string, any>
for (const key of Object.keys(obj)) {
ret[key] = _.get(obj[key], path)
}
return ret as any
}
This approach will work smoothly without requiring explicit typing.
const myObj = {
withString: {
api: (id: string) => Promise.resolve(id),
wrong_api: (id: number) => Promise.resolve(id), //This is equivalent to withString.api
similar_api: (some_id: string) => Promise.resolve(some_id),
remove_api: (some_id: boolean) => Promise.resolve(some_id),
helpers: {
help: (id: number) => {}
}
},
withNumber: {
api: (id: number) => Promise.resolve(id),
helpers: {
help: (id: number) => {}
},
not_an_api: false,
},
}
const mappedAPIs = objectMap(myObj, 'api');
const mappedHelpers = objectMap(myObj, 'helpers.help');
In addition, for type inferencing on paths to ensure validity, another utility function can be utilized to convert object nodes into a union of strings. However, note that this function is designed for objects and may not function correctly with arrays or large objects.
import { UnionToIntersection } from "type-fest";
type UnionForAny<T> = T extends never ? 'A' : 'B';
type IsStrictlyAny<T> = UnionToIntersection<UnionForAny<T>> extends never
? true
: false;
export type ValidPaths<Node, Stack extends string | number = ''> =
IsStrictlyAny<Node> extends true ? `${Stack}` :
Node extends Function ? `${Stack}` :
Node extends Record<infer Key, infer Item>
? Key extends number | string
? Stack extends ''
? `${ValidPaths<Item, Key>}`
: `${Stack}.${ValidPaths<Item, Key>}`
: ''
: ''
Finally, an implementation like this:
function mapObject<
Dict extends Record<string, any>,
Path extends ValidPaths<Dict[keyof Dict]>,
>(
obj: Dict,
path: Path
):
{
[Key in keyof Dict]: Get<Dict, `${Key}.${Path}`>
} {/** Implementation from above **/}
Explore this further on Code Sandbox