Pick value from object with dynamic key based on input key in Typescript

1.3k Views Asked by At

So my backend has a JSON like this

name: "Hello",
nameTranslations: {nl:"Hallo", en: "Hello", fr: "Bonjour"}
description: "Hello",
descriptionTranslations: {nl:"Hallo", en: "Hello", fr: "Bonjour"}

I want to build a function where I'll put in the object and key and it will automatically find the best translation possible. It's easy in Javascript but I want to get Typescript right so I won't be dealing with runtime bugs if I forgot to query the translations (I use GraphQL)

function getText(object, key, language) {
  const best = object[`${key}Translations`][language];
  if (best) {
    return best;
  }

  return object[key];
}

Some Typescript requirements I have

  • I want only the keys possible of the input object
  • I want Typescript to return an error if the key is not available on the object or the 'keyTranslations'

Update Based on the answer of @michmich112 I did go with the following function

function translate<T extends BackendTranslations>(translations: T, fallback: string, language: keyof BackendTranslations) => {
      return translations[language] || fallback;
}

translate(project.nameTranslations, project.name)

I will keep the answer open, since I'm still interested if this could work in Typescript

2

There are 2 best solutions below

4
On BEST ANSWER

Here is my try:

const json = {
  name: "Hello",
  nameTranslations: { nl: "Hallo", en: "Hello", fr: "Bonjour" },
  description: "Hello",
  descriptionTranslations: { nl: "Hallo", en: "Hello", fr: "Bonjour" }
}

type BackendTranslations = {
  nl: string;
  en: string;
  fr: string;
}


type Raw<T> = T extends `${infer R}Translations` ? R : never

/**
 * Once you will have more specific interface for json
 * this overloading will help you,
 * for now it always returns string , because all properties/nested properties of
 * JSON are strings
 */
//function getText<K extends Raw<keyof Data>>(obj: Data, key: K, language: keyof Translations): K extends string ? Data[`${K}Translations`][keyof Translations] : Data[K];

function getText<
  Data extends Record<string, unknown>,
  K extends Raw<keyof Data>
>(obj: Data, key: K, language: keyof BackendTranslations) {
  return (
    (obj[`${key}Translations`] as BackendTranslations)[language] || obj[key]
  );
}

const result = getText(json, 'name', 'en') // ok
const result1 = getText(json, 'nameX', 'en') // error
const result2 = getText(json, 'nameTranslations', 'en') // error
const result3 = getText(json, 'description', 'en') // ok

Please keep in mind, my solutions works only with TypeScript 4.*

2
On

First, you need to create an interface for your JSON:

interface ITranslationData {
  nl: string
  en: string
  fr: string
}

interface ITranslation {
  name: string
  nameTranslations: ITranslationData
  description: string
  descriptionTranslations: ITranslationData
}

once you have that you can use types on your function as follows:

function getText(obj: ITranslation, 
  key: keyof ITranslation,
  language: keyof ITranslationData): string {
  const best = obj[key][language];
  if (best) {
    return best;
  }

  return obj[key];
}

this will require your key to be exact which means you will not be able to do const best = object[${key}Translations][language];

it also means you can write your code in a single line:

function getText(obj: ITranslation, 
  key: keyof ITranslation,
  language: keyof ITranslationData): string {
  return obj[key][language] || obj[key];
}