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?

1 Answer 1

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()
};
Sign up to request clarification or add additional context in comments.

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.