DEV Community

ben hultin
ben hultin

Posted on

TS: Next Level Index Signatures Inside Interfaces and Types

With Typescript we can make our interfaces flexible as to what kind of properties it will accept. Why would this be helpful you may ask, well there are various situations where we need to define the TS compiler to more open to other properties besides the ones defined in the interface. These would include but not limited to:

  • The interface will have more properties than is reasonable to define.
  • We intend to access the properties dynamically via bracket notation myObj[value].

So lets take a look how this can be done

interface Pet {
  // allows any property value of type string
  // what if we want more control over possible properties
  [key: string]: string
}

const myPet: Pet = {
  // all valid values
  // what if we have a general rule about our props naming convention?
  foo: 'foo',
  bar: 'bar',
  kbgyb: '??'
}
Enter fullscreen mode Exit fullscreen mode

This approach maybe a bit too flexible and we have a pattern our properties will look

interface Pet {
  // by adding the prefix 'content-' we can lock down part of the props name
  [key: `content-${string}`]: string
}

const myPet: Pet = {
  'content-type': 'foo', // valid property name
  two: 'bar' // invalid property name as it is not prefixed with 'content-'
}
Enter fullscreen mode Exit fullscreen mode

Let us take this another step further and define the different values our interface / type can accept.

type StringProps = 'type' | 'name' | 'breed';
type StringPet { [key in StringProps]: string }

// same as this 
interface StringPet {
  type: string;
  name: string;
  breed: string;
}

const myPet: Pet = {
  type: 'foo', // valid property name
  two: 'bar' // invalid property name
}
Enter fullscreen mode Exit fullscreen mode

By making use of StringProps we can reduce the interface from 5 lines of code to 2 also without having to write string 3 times as well. This approach can be expanded even further:

type StringProps = 'type' | 'name' | 'breed' | 'eyeColor';
type StringPet { [key in StringProps]: string }

type IntProps = 'age' | 'weight' | 'legs';
type IntPet { [key in IntProps]: number }

type BoolProps = 'wings' | 'claws' | 'fur';
type BoolPet { [key in BoolProps]: boolean }

// here we intersect two types together into one
// note: types are intersected, not extended like interfaces are
type WholePet = IntPet & StringPet & BoolPet;

// WholePet in the more verbose view looks like this
interface WholePet {
  type: string;
  name: string;
  breed: string;
  eyeColor: string;
  age: number;
  weight: number;
  legs: number;
  wings: boolean;
  claws: boolean;
  fur: boolean;
}
Enter fullscreen mode Exit fullscreen mode

The above approach not only brings control over index signature, but can also be used to make our interfaces and types much more scalable. We are not forced to repeat ourselves with primitive types like string, number, or boolean.

If we want to change the primitive type for one of our properties, we simply move it to appropriate Prop type like from IntProps to StringProps.

Thanks for reading!

Top comments (0)