Home > Mobile >  Typesafe constraint on some keys of a Record<string,string> in Typescript
Typesafe constraint on some keys of a Record<string,string> in Typescript

Time:11-05

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 the CountryCode 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 the TranslationKeys 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"
}

Playground

  • Related