More than happy to be informed of a better solution here! But this came up in a real-work situation and I "stumbled" on the solution by more or less guessing.

In plain JavaScript, you have an object which you know you set certain keys on. But because this object is (ab)used for a templating engine, we also put keys/values on it that are not known in advance. In our use case, these keys and booleans came from parsing a .yml file which. It looks something like this:


// Code simplified for the sake of the example

const context = {
  currentVersion: "3.12", 
  currentLanguage: "en",
  activeDate: someDateObject,
  // ... other things that are values of type number, bool, Date, and string
  // ...
}
if (someCondition()) {
  context.hasSomething = true
}

for (const [featureFlag, truth] of Object.entries(parseYamlFile('features.yml')) {
  context[featureFlag] = truth
}

const rendered = render(template: { context })

I don't like this design where you "combine" an object with known keys with a spread of unknown keys coming from an external source. But here we are and we have to convert this to TypeScript, the clock's ticking!

In comes TypeScript

Intuitively, from skimming the simplified pseudo-code above you might try this:


type Context = {
  currentVersion: string
  currentLanguage: string
  activeDate: Date
  [featureFlag: string]: boolean
}

TS error

TypeScript Playground demo here

Except, it won't work:

Property 'currentVersion' of type 'string' is not assignable to 'string' index type 'boolean'.
Property 'currentLanguage' of type 'string' is not assignable to 'string' index type 'boolean'.
Property 'activeDate' of type 'Date' is not assignable to 'string' index type 'boolean'.

Make sense, right? We're saying the type should have this, that, and that, but also saying that it can be anything. So it's a conflict.

How I solved it

I'll be honest, I'm not sure this is very intuitive, either. But it works:


type FeatureFlags = {
  [featureFlag: string]: boolean
}
type Context = FeatureFlags & {
  currentVersion: string
  currentLanguage: string
  activeDate: Date
}

TypeScript Playground demo

It does imply that the inheritance, using the & is more than just semantic sugar. It means something.

Comments

Your email will never ever be published.

Related posts