TypeScript object types with optional and minimally required properties
TL;DR
// Solution 1
type LabelEntry = { label: string };
type ImageEntry = { image: string };
type Entry = LabelEntry | ImageEntry;
// Solution 2
type Entry = Record<'label' & 'image', string>;
An object type that I find myself creating very frequently is one that has one or all of several properties. For example, I can create an entry with a text label, one with an image, or one with both:
const textEntry = {
label: 'Fluffy cloud',
};
const imgEntry = {
image: 'cloud.jpg',
};
const bothEntry = {
label: 'Fluffy cloud',
image: 'cloud.jpg',
};
The problem
Intuituively, I would declare the type for this kind of objects to be like this:
type Entry = {
label?: string;
image?: string;
};
There's a problem with this type. You can see the problem in the next listing with entry3
:
const entry1: Entry = {
label: 'Fluffy cloud',
};
const entry2: Entry = {
image: 'cloud.jpg',
};
const entry3: Entry = {};
// We don't want to allow this to happen.
With this type definition, entry3
can be assigned an empty object. This is not the outcome that we want.
Solution 1
One solution to the problem is to create two types and create an intersection type like so:
type LabelEntry = {
label: string;
};
type ImageEntry = {
image: string;
};
type Entry = LabelEntry | ImageEntry;
The entry3
variable above will show an error Type '{}' is not assignable to type 'Entry'.
because it contains an empty object.
Instead it must have either one (or both) label
or image
as its property.
const entry1: Entry = {
label: 'Fluffy cloud',
};
const entry2: Entry = {
image: 'cloud.jpg',
};
const entry3: Entry = {
label: 'Fluffy cloud',
image: 'cloud.jpg',
};
Solution 2
If coming up with names is not your strongest forte or you want to have fewer redundant lines of code, this might be your preferred solution:
type Entry = Record<'label' & 'image', string>;
Record
is an utility type that helps to define the keys and values of an object type easily.
P.S. A mistake that I made before was using union (
|
) instead of intersection (&
) for the key values (i.e.Record<'label' | 'image', string>
). This would have madeentry1
andentry2
invalid.