Home > Back-end >  Add type-safety for nested object-type
Add type-safety for nested object-type

Time:01-27

I want to build a translation-library, ideally while maintaining full type-safety. This is what I have so far:

// Want to add translations for a new language?
// Just add "| 'gr'"
type Lang = "de" | "en";

type Fields = {
  // We can define sub-entries for each new context. history, orderManagement,
  // vehicleDetails... whatever makes sense.
  history: {
    historyEntry: string;
    locationChanged: string;
    manuallyCreated: string;
  };
  orderManagement: {
    // If there is a new string to translate, we add a new field of type string
    order: string;
  };
};

// This is the "root-type" that binds each lang to the fields-object
type Translations = {
  [key in Lang]: Fields;
};

// If you delete one single entry, you will get an error.
// The type "Translations" makes sure that you don't forget
// a single field
const translations: Translations = {
  de: {
    history: {
      locationChanged: "Standort geändert",
      manuallyCreated: "Manuell erstellt",
    },
    orderManagement: {
      order: "Auftrag",
    },
  },
  en: {
    history: {
      locationChanged: "Location Changed",
      manuallyCreated: "Manually Created",
    },
    orderManagement: {
      order: "Order",
    },
  },
};

// TODO: How can this be more type-safe?
export const getTranslations = (lang: Lang, context: string, field: string) => {
  return translations[lang][context][field];
}

how can I refactor the Fields-type, so that the getTranslations function is more typesafe?

Right now I could call getTranslations like this:

const foo = getTranslations("de", "bar", "baz");

without noticing the error at build-time / in the text-editor. Making this more type-safe would also bring the benefit of enabling autocompletion in the text-editor.

CodePudding user response:

You don't need to change the Fields you should instead make getTranslations generic to capture the context the user of getTranslations wants to get. YOu need to specify that this type parameter has to extend keyof Fields. You can then use an indexed type to type field to be one of the fields of the previously specified context:


export const getTranslations = <C extends keyof Fields>(lang: Lang, context: C, field: keyof Fields[C]) => {
  return translations[lang][context][field];
}

const t1 = getTranslations("de", "history", "locationChanged"); //ok
const t2 = getTranslations("de", "orderManagement", "locationChanged"); // error

Playground Link

You can also make the second parameter be a . separated path to the field:

type Path<T> = keyof {
  [P in keyof T & string as `${P}.${keyof T[P] & string}`]: never
}
export const getTranslations = (lang: Lang, path: Path<Fields>) => {
  let [context, field] = path.split('.') as [keyof Fields, keyof Fields[keyof Fields]]
  return translations[lang][context][field];
}

const t1 = getTranslations("de", "history.locationChanged"); //ok
const t2 = getTranslations("de", "orderManagement.locationChanged"); // error
const t3 = getTranslations("de", "orderManagement.order"); //ok

Playground Link

  • Related