publicJorn
publicJorn

Reputation: 2494

TS: get type of object property by variable

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

Answers (1)

Chase
Chase

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

Related Questions