Выберите значение из объекта с динамическим ключом на основе ключа ввода в Typescript

Итак, мой бэкэнд имеет такой JSON

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

Я хочу создать функцию, в которую я вставлю объект и ключ, и она автоматически найдет наилучший возможный перевод. Это просто в Javascript, но я хочу правильно настроить Typescript, чтобы не сталкиваться с ошибками во время выполнения, если я забыл запросить переводы (я использую GraphQL)

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

  return object[key];
}

Некоторые требования Typescript у меня есть

  • Мне нужны только возможные ключи объекта ввода
  • Я хочу, чтобы Typescript возвращал ошибку, если ключ недоступен для объекта или «keyTranslations»

Обновление Основываясь на ответе @michmich112, я использовал следующую функцию

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

translate(project.nameTranslations, project.name)

Я оставлю ответ открытым, так как мне все еще интересно, может ли это работать в Typescript


person Richard Lindhout    schedule 18.12.2020    source источник
comment
Что не так с ответом @michmich?   -  person captain-yossarian    schedule 18.12.2020
comment
Это действительно решило мою проблему с его предложением, но не ответило на вопрос, и мне все еще интересно, может ли исходная функция быть безопасной для типов, а также следовать моим требованиям.   -  person Richard Lindhout    schedule 18.12.2020


Ответы (2)


Вот моя попытка:

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

Имейте в виду, что мои решения работают только с TypeScript 4.*

person captain-yossarian    schedule 18.12.2020
comment
Действительно хорошо ‹3, спасибо. Если вы измените его на приведенный ниже код, я приму ответ. Он автоматически увидит, какой входной объект помещается в функцию ``` 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] ); } ``` - person Richard Lindhout; 19.12.2020
comment
@RichardLindhout Я принял ваши предложения. Теперь все в порядке - person captain-yossarian; 19.12.2020
comment
Большое спасибо, Typescript 4 отлично справляется с литералами шаблонов, но я не понимаю, как их писать, спасибо за помощь! - person Richard Lindhout; 19.12.2020
comment
Вы можете найти оригинальные PR для шаблонных строк, примеров очень много. Также взгляните на документы TS - person captain-yossarian; 19.12.2020

Во-первых, вам нужно создать интерфейс для вашего JSON:

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

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

как только вы это сделаете, вы можете использовать типы в своей функции следующим образом:

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

  return obj[key];
}

для этого потребуется, чтобы ваш ключ был точным, что означает, что вы не сможете выполнять const best = object[${key}Переводы][language];

это также означает, что вы можете написать свой код в одну строку:

function getText(obj: ITranslation, 
  key: keyof ITranslation,
  language: keyof ITranslationData): string {
  return obj[key][language] || obj[key];
}
person michmich112    schedule 18.12.2020
comment
Я думаю, что согласен с мнением об использовании другой функции - person Richard Lindhout; 18.12.2020
comment
Спасибо за ваше время и предложения! - person Richard Lindhout; 18.12.2020