2

I'm looking to take an object that looks like this:

const modules = {
  Foo: {
    dataSources: {
      DataSourceA,
      DataSourceB
    }
  },
  Bar: {
    dataSources: {
      DataSourceC,
      DataSourceD
    }
  }
};

And turn it into this:

const dataSources = {
  DataSourceA: new DataSourceA(),
  DataSourceB: new DataSourceB(),
  DataSourceC: new DataSourceC(),
  DataSourceD: new DataSourceD()
};

While maintaining the types on each DataSource. The mapping itself is not a concern it's distilling the type of each DataSource from the larger module object to create a type representing the instances of each DataSource.

If both Foo and Bar in modules are of type IModule then the following:

type Foo<T extends { [K in keyof T]: IModule }> = {
  [K in keyof T]: new () => T[keyof T]["dataSources"]
};

import * as modules from "./modules";

const dataSources: Foo<typeof modules> = {
  DataSourceA: new modules.Foo.dataSources.DataSourceA()
};

Is close, but I get the following error: Type 'DataSourceA' provides no match for the signature 'new (): typeof import("src/modules/Foo/datasources/index") which leads me to believe that my type is trying to point to a constructor in the dataSources module, not the classes defined in the module.

Can anyone help me understand where I've gone wrong here?

adampetrie
  • 1,130
  • 1
  • 11
  • 23

1 Answers1

1

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()
};
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 1
    Thank you so much for taking the time write such a thorough answer that not only solved my problem but also walked me through it so that I could learn along the way. Never underestimate the kindness of strangers on the internet :P. – adampetrie Jan 05 '19 at 16:36