Home > database >  How to infer computed object keys in TypeScript
How to infer computed object keys in TypeScript

Time:06-11

I am creating a project in which I want to easily define new plugins and I want to use TypeScript in my IDE to help me use the names of these plugins. I have a folder structure like this:

src
│ ...
└── plugins
   └── pluginA
   |     index.ts
   └── pluginB
   |     index.ts
   └── ...
     index.ts <-- "here I want to combine all plugins in one object"

Each plugin has a default export in index.ts with interface like this:

interface Plugin {
  name: string
  ...
}

src/plugins/pluginA/index.ts

export default {
  name: "pluginA",
  ...
}

src/plugins/index.ts

import pluginA from "./pluginA"
import pluginB from "./pluginB"
...

export default {
  [pluginA.name]: pluginA
  [pluginB.name]: pluginB
  ...
}

And I want TypeScript to understand what the names of the existing plugins (to infer the Literal type of keys of the plugins).

In other words, I want to use defined plugins in my code like this:

import plugins from "src/plugins"

...

const plugin = plugins["nonExistingPlugin"] // I want the typescript to show error here, since there is no plugin with this name
//                     ~~~~~~~~~~~~~~~~~~~

const anotherPlugin = plugins["plug_"] // I want the IDE to autocomplete the name of the plugin from the existing ones
//                            "pluginA"
//                            "pluginB"
//                            "..."

All my attempts led me to the fact that TypeScript understood that the name property of the plugin is a string, but didn't infer Literal type.

Can TypeScript do that? And if not, is there any other way to achieve this?

CodePudding user response:

You can use as const assertions here:

const pluginA = {
  name: 'pluginA',
  //
} as const
const pluginB = {
  name: 'pluginB',
  //
} as const
const pluginC = {
  name: 'pluginC',
  //
} as const

const plugins = {
  [pluginA.name]: '',
  [pluginB.name]: '',
  [pluginC.name]: '',
}
const plugin = plugins["nonExistingPlugin"] 

// Gives me a type error as expected
// Element implicitly has an 'any' type because expression of type '"nonExistingPlugin"' can't be used to index type '{ pluginA: string; pluginB: string; pluginC: string; }'.
//  Property 'nonExistingPlugin' does not exist on type '{ pluginA: string; pluginB: string; pluginC: string; }'.ts(7053)
const plugin = plugins["plugin"] // Editor shows autocompletes for `pluginA`, `pluginB`, `pluginC`

CodePudding user response:

I would suggest using enum for that:

types.d.ts

export enum PluginNames {
   PluginA = 'pluginA',
   PluginB = 'pluginB',
   PluginC = 'pluginC',
}

interface Plugin {
   name: PluginNames
}

src/plugins/pluginA/index.ts

const pluginA: Plugin = {
  name: PluginNames.PluginA
}

export default pluginA

src/plugins/index.ts

import pluginA from "./pluginA"
import pluginB from "./pluginB"
...

export default {
  [PluginNames.PluginA]: pluginA
  [PluginNames.PluginB]: pluginB
  ...
}

You can also do the same using type unions:

interface Plugin {
   name: 'pluginA' | 'pluginB' | 'pluginC'
}
  • Related