Search code examples
typescriptdynamic-typingconditional-types

Create typings dynamically from object


Say I have the following two types:

export type CollectionNames = 'twitter:tweets' | 'twitter:users' | 'twitter:metadata-cashtag'

export type CollectionType<T extends CollectionNames> = 
    T extends 'twitter:tweets' ? Tweet :
    T extends 'twitter:users' ? User :
    T extends 'twitter:metadata-cashtag' ? CashtagMetadataDb :
    never

I feel this is very clunky and I'm not very keen on having the strings twice. Also it's possible to legally misspell them in the latter type.

Is there any way to create these dynamically from an object such as this:

typings = {
    'twitter:tweets': Tweet,
    'twitter:users': User,
    'twitters:metadata-cashtag': CashtagMetadataDb
}

The idea is that multiple modules will have their own CollectionType type which is then aggregated into one CollectionType in the importing root module. So if I have two modules Coin and Twitter imported using * as, it looks something like this:

type CollectionName = Twitter.CollectionNames | Coin.CollectionNames

type CollectionType<T extends CollectionName> = 
    T extends Twitter.CollectionNames ? Twitter.CollectionType<T> :
    T extends Coin.CollectionNames ? Coin.CollectionType<T> :
    never

These will then be used in a function like so where the types are of the latter kind (Collection here is from MongoDB):

async function getCollection<T extends CollectionName> (name: T): Promise<Collection<CollectionType<T>>>

Solution

  • I think in this case you don't need conditional types at all; you can do this with keyof and lookup types instead. You probably could create an object like typings and derive a type from it, but unless you need that object for something at runtime (and have objects of type Tweet, User, etc lying around) I'd say you should just make an interface type like this:

    export interface Collections {
      'twitter:tweets': Tweet,
      'twitter:users': User,
      'twitter:metadata-cashtag': CashtagMetadataDb
    }
    

    Then, your CollectionNames and CollectionType types can be defined in terms of that type:

    export type CollectionNames = keyof Collections;
    export type CollectionType<K extends CollectionNames> = Collections[K];
    

    You can verify that the above types act the same as your definitions. In the case that you have multiple modules that have exported Collections types, you can simply merge them using interface extension and re-derive CollectionNames and CollectionType from it:

    export interface Collections extends Twitter.Collections, Coin.Collections {}
    export type CollectionNames = keyof Collections;
    export type CollectionType<K extends CollectionNames> = Collections[K];
    

    Hope that helps. Good luck!