TS: Next Level Index Signatures Inside Interfaces and Types
ben hultin
Posted on March 27, 2023
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: '??'
}
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-'
}
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
}
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;
}
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!
Posted on March 27, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 11, 2024