Skip to main content

TypeScript object types with optional and minimally required properties

· 3 min read

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 made entry1 and entry2 invalid.