#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/hmzLM5. @publicJorn Что такое короткий ключ?
_featureNames
необходимо для созданияIGlobals
интерфейса (который содержит имена функций).featureNames
это просто версия массива_featureNames
, ничего особенного. Это необходимоisValidFeature
для безопасной проверки правильности имени объекта.