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
}
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
}
It does imply that the inheritance, using the &
is more than just semantic sugar. It means something.
Comments