Reputation: 2494
I'm new in an existing project, where there's some globals defined that come from the backend. It has a corresponding interface:
interface IGlobals {
feature_foo: boolean
feature_bar: boolean
someOtherProp: string
}
const globals: IGlobals = {
feature_foo: true,
feature_bar: false,
someOtherProp: 'hello'
}
Now I want to write a function that checks if a certain feature flag exists on that global object. I did that assuming that any property starting with feature_
will always be a boolean (which in our case it true):
// 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
}
While this works, it feels like a bit of a hack.
I'm mostly bothered by the as boolean
statement. Which I used because otherwise TS will rightly complain the value could be a boolean or string, which doesn't match with the return value of the function.
Can it be made better?
I have a feeling this may be done with "lookups", but I don't fully grasp that concept.
Upvotes: 1
Views: 316
Reputation: 5615
Sure! You can make this fully type-safe and completely ignore the need to even have a .hasOwnProperty
lookup-
function useFeature(feature: 'foo' | 'bar'): boolean {
const featureKey = `feature_${feature}` as const;
return globals[featureKey];
}
(OR)
function useFeature(feature: 'foo' | 'bar'): boolean {
return globals[`feature_${feature}`];
}
That's a template literal type, being used as a key. If you restrict the feature
parameter to the exact feature names, you have it fully type-safe. However, if you would like to be more lenient on the feature
argument and do an if
check within, you should have this helper function for narrowing types-
function isValidFeature(x: string): x is 'foo' | 'bar' {
return x == 'foo' || x == 'bar';
}
Remember to update the feature list here when you update them in IGlobals
!
Then, you can just do-
function useFeature(feature: string): boolean {
return isValidFeature(feature) && globals[`feature_${feature}`];
}
No casting needed, all type safe and checked thoroughly.
Edit:-
You know what would be better though? Establishing a connection between isValidFeature
and IGlobals
directly so you can update features at one place and have it be reflected at the other place. This is where we get into type level programming-
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);
}
Now, you can just update feature names within the _featureNames
tuple and everything will get auto updated. Neat!
Check it out on playground.
Upvotes: 2