There are two problems in the code. the first one is getting the instance type from a class (Since my understanding is that DataSource* are classes). To do that we can use the build-in conditional type InstanceType
. So for example:
const ds = DataSourceA; // typed as typeof DataSourceA
type dsInstance = InstanceType<typeof ds> // is DataSourceA
The second part is flattening the module structure. The first thing we need to do is apply a mapped type to get all data source instance types:
type IDataSources = {[name: string]: new (...a: any[]) => any }
type DataSourceInstances<T extends IDataSources> = {
[P in keyof T] : InstanceType<T[P]>
}
//The type below is the same as { DataSourceC: DataSourceC; DataSourceD: DataSourceD; }
type dsModules = DataSourceInstances<typeof modules['Bar']['dataSources']>
So now we can get the instances for all the data sources in a module. We can also get a union of all data sources in all modules in a similar way, if we use keyof typeof modules
instead of a specific module name:
//The type below is the same as {DataSourceA: DataSourceA;DataSourceB: DataSourceB;} | {DataSourceC: DataSourceC;DataSourceD: DataSourceD;}
type dsModules = DataSourceInstances<typeof modules[keyof typeof modules]['dataSources']>
But we obviously don't want a union, we would want to have all those data sources in a single object. If we can convert the union to an intersection we would basically be there. We can do this with a little help from jcalz's UnionToIntesection
(upvote his answer here)
type UnionToIntersection<U> = (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type AllDataSources<T extends { [K in keyof T]: IModule }, U = DataSourceInstances<T[keyof T]['dataSources']>> = Id<UnionToIntersection<U>>
//below is same as { DataSourceA: DataSourceA; DataSourceB: DataSourceB } & { DataSourceC: DataSourceC; DataSourceD: DataSourceD;}
type moduleDs = AllDataSources<typeof modules>
Now this will work as expected but if you hover over moduleDs
you will see a very ugly and confusing type:
DataSourceInstances<{
DataSourceA: typeof DataSourceA;
DataSourceB: typeof DataSourceB;
}> & DataSourceInstances<{
DataSourceC: typeof DataSourceC;
DataSourceD: typeof DataSourceD;
}>
If you want to flatten it out to get better tooltips (and for that reason alone) you can use the trick described here by Nurbol Alpysbayev (again I encourage you to upvote his answer :) )
Putting it altogether we get:
type IModule = { dataSources: IDataSources }
type IDataSources = {[name: string]: new (...a: any[]) => any }
type DataSourceInstances<T extends IDataSources> = {
[P in keyof T] : InstanceType<T[P]>
}
type Id<T> = {} & { [P in keyof T]: T[P] }
type UnionToIntersection<U> = (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type AllDataSources<T extends { [K in keyof T]: IModule }, U = DataSourceInstances<T[keyof T]['dataSources']>> = Id<UnionToIntersection<U>>
//tooltip will be { DataSourceA: DataSourceA; DataSourceB: DataSourceB; DataSourceC: DataSourceC; DataSourceD: DataSourceD;}
const dataSources: AllDataSources<typeof modules> = {
DataSourceA: new DataSourceA(),
DataSourceB: new DataSourceB(),
DataSourceC: new DataSourceC(),
DataSourceD: new DataSourceD()
};