Home > Back-end >  Derive exact types from a typed object
Derive exact types from a typed object

Time:01-09

In TypeScript, I want to derive types from constants like this

const iso3ToCountry = {
  DEU: {
    name: "Germany",
    continent: "Europe",
    language: "de",
  },
  GBR: {
    name: "United Kingdom of Great Britain and Northern Ireland",
    continent: "Europe",
    language: "en",
  },
} as const;

type ISOCode = keyof typeof iso3ToCountry // same as "DEU" | "GBR";

But, I also want to ensure that the values of the object implement the following interface:

interface CountryDetails {
  name: string;
  continent: string;
  language: string;
}

The only way, that I have found to achieve both, is to use a helper function

function ensureType<T extends Record<string, CountryDetails>>(obj: T): T {
  return obj;
}

const iso3ToCountry = ensureType({
  DEU: {
    name: "Germany",
    continent: "Europe",
    language: "de",
  },
  GBR: {
    name: "United Kingdom",
    continent: "Europe",
    language: "en",
  },
} as const);

type ISOCode = keyof typeof iso3ToCountry;

Now ISOCode is equivalent to "DEU" | "GBR" and every value in the iso3ToCountry must have a name, continent and a language.

The downside is that the ensureType function will be in the compiled JavaScript. We add code to our app, just to solve some compile-time problems.

Question:

Can I achieve type-checks in the "iso3ToCountry"-object and the ability to derive the key-type without having to add code that will remain in the compiled JavaScript.

CodePudding user response:

You really want the so-called satisfies operator, as requested in microsoft/TypeScript#7481, but this does not currently exist in TypeScript (as of 4.5). The generic helper function method you are using is a very common workaround for this, especially because it works for just about any expression. The extra function call in the emitted JavaScript is usually not a big deal, but you're looking for something with zero runtime impact.


A different workaround is to define a type using the typeof type operator on a variable of the same type as the expression you want to check, which will succeed or fail to compiler depending on your desired constraint.

You can't use the typeof operator on arbitrary expressions; it has to be a variable or a property of a variable. So this workaround could have some runtime impact if you need to declare a new variable.

Since you already have a variable named iso3ToCountry of the right type, you don't have to define a new variable, so there's no runtime impact.

Here's one way to do it:

type CheckIso3ToCountry<T extends Record<keyof T, CountryDetails> =
  typeof iso3ToCountry> = void; 
//^^^^^^^^^^^^^^^^^^^^ <-- you've got a problem with 
// iso3ToCountry if and only if there's an error here

Here the CheckIso3ToCountry<T> type is a dummy type that evaluates to void no matter what. The important parts are the generic constraint on the T type parameter forcing it to extend Record<keyof T, CountryDetails>, and the generic parameter default of typeof iso3ToCountry. As long as the latter type is assignable to the former, everything's fine.

But if you make a mistake with iso3ToCountry:

const iso3ToCountry = {
  DEU: {
    name: "Germany",
    continent: "Europe",
    language: "de",
  },
  GBR: {
    name: "United Kingdom of Great Britain and Northern Ireland",
    continant: "Europe",
    language: "en",
  },
} as const;

Then you'll get an error:

type CheckIso3ToCountry<T extends Record<keyof T, CountryDetails> =
  typeof iso3ToCountry> = void; // error!
//~~~~~~~~~~~~~~~~~~~~ <--
// Property 'continent' is missing in type 
// '{ readonly name: "United Kingdom of Great Britain and Northern Ireland";
//  readonly continant: "Europe"; readonly language: "en"; }' 
// but required in type 'CountryDetails'.

Playground link to code

  •  Tags:  
  • Related