I have x number of modules that are used in my AWS lambda functions such as a UserModule
, NotificationsModule
, CompanyModule
, etc. I created an interface that describes the structure of the modules as the following:
interface Modules {
company: ICompanyModule
user: {
base: IUserModule
}
notifications: {
settings: INotificationSettingsModule
details: INotificationDetailsModule
}
}
Let's say I need the company and user base module in a function I would have to import both manually and instantiate them. If I had just a handful of functions that would work but that is not the case. To decrease the amount of imports and manual work I thought that I would create a factory
method that would return an object with only the modules that the function needs. For example:
// Single module
const module = factory('company') // Type would be ICompanyModule
// Multiple Modules
const modules = factory(['company', 'user.base']) // Type would be { company: ICompanyModule, 'user.base': IUserModule }
The input to the factory method would be an array of keys in the Modules
interface.
I'm about to get to a "solution" but I can't seem to fix a Type instantiation is excessively deep and possibly infinite
error. Below is what I currently have:
// This gets all the keys and nested keys of my Modules interface
type Path<T, Key extends keyof T = keyof T> = Key extends string
? T[Key] extends Record<string, unknown>
?
| `${Key}.${Path<T[Key], Exclude<keyof T[Key], keyof Array<unknown>>> & string}`
| `${Key}.${Exclude<keyof T[Key], keyof Array<unknown>> & string}`
| Key
: Key
: never
// This gets the value for the Path wether it be nested or not
type PathValue<T, P extends Path<T>> = P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Rest extends Path<T[Key]>
? PathValue<T[Key], Rest>
: never
: never
: P extends keyof T
? T[P]
: never
// This is my ReturnType that takes in the a single key or array of keys and builds the object type
type ReturnObject<T extends Object, P extends Path<T> | Array<Path<T>>> = P extends Array<Path<T>>
? {
[Key in P[number]]: Key extends keyof T
? T[Key] extends Object
? Key extends Path<T[Key]>
? ReturnObject<T[Key], Key>
: T[Key]
: never
: Key extends `${infer K}.${infer Rest}`
? K extends keyof T
? T[K] extends Object
? Rest extends Path<T[K]>
? ReturnObject<T[K], Rest>
: never
: never
: never
: never
}
: P extends keyof T
? T[P]
: P extends `${infer K}.${infer Rest}`
? K extends keyof T
? T[K] extends Object
? Rest extends Path<T[K]>
? ReturnObject<T[K], Rest>
: never
: T[K]
: never
: never
// This is the factory function declaration
declare function factory<T extends ModuleFactory, P extends Path<T> | Array<Path<T>>>(
modules: P
): ReturnObject<T, P>
Now this works. When I create call factory
with any of the given keys I get the correct object structure back but Typescript is throwing the Type instantiation is excessively deep and possibly infinite
error in the ReturnType
at { [Key in P[number]]: ... }
After a few days of messing around with this my best guess is that the issue is that P can extend Array<Path<T>> and Typescript is trying to handle the case the the array passed in could have a huge length and in turn cause almost infinite recursion.
You may use non-fallible path getter Get
to obtain the value and generic type validator conforms
to allow only proper paths
type conforms<T, V> = T extends V ? T : V;
type validatePath<Path, Obj> =
| Path extends keyof Obj ? Path
: Path extends `${infer F extends keyof Obj & string}.${infer L}` ? `${F}.${validatePath<L, Obj[F]>}`
: keyof Obj & string
type Get<Obj, Path> =
| Path extends `${infer F}.${infer L}` ? Get<Get<Obj, F>, L>
: Path extends keyof Obj ? Obj[Path]
: [Obj, Path]
function factoryS<Path extends string>(path: conforms<Path, validatePath<Path, Modules>>): Get<Modules, Path> {
return null!;
}
function factoryA<const A>(list: conforms<A, { [K in keyof A]: validatePath<A[K], Modules> }>)
: A extends readonly string[] ? { [K in A[number]]: Get<Modules, K> } : never {
return null!;
}
// you may make them single function overloads