TS: получение типа свойства объекта по переменной

#typescript

Вопрос:

Я новичок в существующем проекте, где определены некоторые глобальные переменные, которые поступают из серверной части. Он имеет соответствующий интерфейс:

 interface IGlobals {
    feature_foo: boolean
    feature_bar: boolean
    someOtherProp: string
}

const globals: IGlobals = {
    feature_foo: true,
    feature_bar: false,
    someOtherProp: 'hello'
}

 

Теперь я хочу написать функцию, которая проверяет, существует ли определенный флаг объекта для этого глобального объекта. Я сделал это, предполагая, что любое свойство, начинающееся с feature_ , всегда будет логическим (что в нашем случае является истинным):

 // This is called as: `const isEnabled = useFeature('foo')`
function useFeature(feature: string): boolean {
  const featureKey = `feature_${feature}`

  if (globals.hasOwnProperty(featureKey)) {
    // global starting with `feature_` is always boolean
    return globals[featureKey as keyof IGlobals] as boolean
  }

  return false
}
 

Хотя это работает, это похоже на небольшой взлом.
Меня больше всего беспокоит это as boolean утверждение. Который я использовал, потому что в противном случае TS будет справедливо жаловаться, что значение может быть логическим или строковым, которое не совпадает с возвращаемым значением функции.

Можно ли сделать это лучше?

У меня такое чувство, что это может быть сделано с помощью «поиска», но я не до конца понимаю эту концепцию.

Ответ №1:

Конечно! Вы можете сделать это полностью типобезопасным и полностью игнорировать необходимость даже .hasOwnProperty поиска-

 function useFeature(feature: 'foo' | 'bar'): boolean {
  const featureKey = `feature_${feature}` as const;

  return globals[featureKey];
}
 

(ИЛИ)

 function useFeature(feature: 'foo' | 'bar'): boolean {
  return globals[`feature_${feature}`];
}
 

Это тип литерала шаблона, используемый в качестве ключа. Если вы ограничиваете feature параметр точными именами объектов, он полностью типобезопасен. Однако, если вы хотите быть более снисходительным к feature аргументу и выполнить if проверку внутри, у вас должна быть эта вспомогательная функция для сужения типов-

 function isValidFeature(x: string): x is 'foo' | 'bar' {
  return x == 'foo' || x == 'bar';
}
 

Не забудьте обновить список функций здесь, когда вы обновляете их в IGlobals !

Затем вы можете просто сделать-

 function useFeature(feature: string): boolean {
  return isValidFeature(feature) amp;amp; globals[`feature_${feature}`];
}
 

Приведение не требуется, все типы безопасны и тщательно проверены.

Редактировать: — Вы знаете, что было бы лучше? Установление связи между isValidFeature и IGlobals напрямую, чтобы вы могли обновлять функции в одном месте и отражать их в другом месте. Здесь мы переходим к программированию на уровне типов-

 const _featureNames = ['foo', 'bar'] as const;
const featureNames: string[] = [..._featureNames];

type FeatureKeys = {
  [featureName in typeof _featureNames[number]]: `feature_${featureName}`;
}[typeof _featureNames[number]]

type Features = {
  [featureKey in FeatureKeys]: boolean;
}

interface IGlobals extends Features {
    someOtherProp: string;
    andAnotherOne: number;
}

declare const globals: IGlobals;

function isValidFeature(x: string): x is typeof _featureNames[number] {
  return featureNames.includes(x);
}
 

Теперь вы можете просто обновить имена объектов внутри _featureNames кортежа, и все будет обновлено автоматически. Аккуратно!

Проверьте это на игровой площадке.

Комментарии:

1. Спасибо за редактирование! Я должен был упомянуть, что я не хотел переписывать имена объектов. Это выглядит потрясающе! Я немного смущен FeatureKeys объявлением типа. Используется ли массив в конце для «заполнения» записей внутри? Можете ли вы это объяснить? Приветствия!

2. @publicJorn Да, это всего лишь немного программирования на уровне типов. По сути, сначала он использует типы сопоставления для создания объекта, в котором каждое значение feature_${featureName} (для всех имен объектов). Таким образом, объект будет выглядеть как — { foo: 'feature_foo'; bar: 'feature_bar' } в этом случае. Затем мы индексируем тип объекта по его ключевому типу. В typescript это, по сути, возвращает тип значения объекта. Поскольку значения объекта являются постоянными строками 'feature_foo' | 'feature_bar' , в данном случае он возвращает их объединение. Обратите внимание, как { [k: string]: number | boolean }[string] это дает вам number | boolean .

3. Мне пришлось прочитать это несколько раз, но теперь я понял 🙂 Спасибо!

4. все еще пытаюсь полностью понять это.. @Chase, если вы будете так любезны 🙂 Возможно ли вышеуказанное, если отдельно не предоставлять _featureNames массив? В идеале введенный «короткий ключ», используемый в useFeature , является локальным только для этой функции. Ссылка на игровую площадку: shorturl.at/hmzLM

5. @publicJorn Что такое короткий ключ? _featureNames необходимо для создания IGlobals интерфейса (который содержит имена функций). featureNames это просто версия массива _featureNames , ничего особенного. Это необходимо isValidFeature для безопасной проверки правильности имени объекта.