Let's say I have a COUNTRIES const array somewhere :
const COUNTRIES = [
"fr", "sp", "de", "uk"
] as const;
type CountryCode = typeof COUNTRIES[number];
And somewhere else, I have a big Record<string,string>
map containing a bunch of translations for my app, something like :
const EN_TRANSLATIONS: Record<TranslationKeys, string> = {
"hello-world": "Hello world !",
"what-time": "What time is it ?",
"countries.fr": "France",
"countries.sp": "Spain",
"countries.de": "Germany",
"countries.uk": "United Kingdom"
}
Is it possible to define the TranslationKeys
type above which would enforce following rules at compilation time :
- I should be able to define any string as the key without having to white list those in a union type (I don't want to maintain a dedicated union type for all of my keys)
- There should be some "special" keys following special conventions, and coupled to some type constraints. In my example, I would like that
countries.*
labels should represent the exhaustive list of COUNTRIES entries (matching theCountryCode
union type) and, if this is not the case, I would like the compiler to complain that some country labels are missing (that way, whenever I add a new country code, I will not forget to provide its translation label) - If possible, I would like to keep my
EN_TRANSLATIONS
type as is (I mean, only play with theTranslationKeys
type) : typically, I know that one solution would be to introduce some (typed) variable and use spread operator like this :
const COUNTRY_EN_TRANSLATIONS: {[key in `countries.${CountryCode}`]: string} = {
"countries.fr": "France",
"countries.sp": "Spain",
"countries.de": "Germany",
"countries.uk": "United Kingdom"
}
const EN_TRANSLATIONS: Record<string, string> = {
"hello-world": "Hello world !",
"what-time": "What time is it ?",
...COUNTRY_EN_TRANSLATIONS
}
but I would like to avoid this (if possible).
I made some trials with the new satisfies
keyword introduced in upcoming TS 4.9 version but it doesn't seem to play well either.
Any idea on how I could workaround this ?
Thanks in advance for your help :)
CodePudding user response:
You're close. But instead of simply a Record
, you can do this by using a mapped type:
const COUNTRIES = [
"fr", "sp", "de", "uk"
] as const;
type CountryCode = typeof COUNTRIES[number];
type MustHaveCountries = Record<string, string> & {
[key in `countries.${CountryCode}`]: string;
}
const valid: MustHaveCountries = {
"countries.ca": "foo",
"countries.de": "foo",
"countries.fr": "foo",
"countries.sp": "foo",
"countries.uk": "foo",
'bar': "bar"
}
const invalid: MustHaveCountries = {
"countries.ca": "foo",
"countries.de": "foo",
"countries.fr": "foo",
"countries.sp": "foo",
// "countries.uk": "foo", ERROR: Property '"countries.uk"' is missing in type
'bar': "bar"
}