circuit

How to Make Certain Properties Optional in TypeScript




Photo by Kevin Ku on Unsplash

Photo by Kevin Ku on Unsplash

The declaration of the interface is a place to make some properties optional. You can achieve that with a question mark next to the properties names.

interface Person {
    uuid: string;
    name: string;
    surname: string;
    sex: string;
    height: number;
    age: number;
    pet?: Animal;
}

interface Animal {
    name: string;
    age: number;
}

In the above example, the pet is an optional property. It's useful, but every time you would like to perform some operations on the pet of the Person you need to check if it's declared, since pet?: Animal; is a shorthand to the pet: Animal | undefined; syntax.

Little case study

Let's assume that you want to make a function that will return the name initial and surname like J. Doe.

function personShortName(name: string, surname: string): string {*
    *return `${name[0]} ${surname}`;
}

const person1: Person = { /* Person properties */ };

personShortName(person1.name, person1.surname);

Instead of accessing name and surname each time you call thepersonShortName() we can use the Person type object as the function argument.

function personShortName(person: Person): string {*
    *return `${person.name[0]} ${person.surname}`;
}

We could, of course, also use destructuring to simplify this function like this:

function personShortName({name, surname}: Person): string {*
    *return `${name[0]} ${surname}`;
}

Seems good but look at the limitation. You need an object of Person type to call a simple function. And now, if you would like to call it elsewhere, you would need to create a new Person.

❌ personShortName({ name: 'Robert', surname: 'Doe' })

The above example won't work since { name: string, surname: string } doesn't match the Person type.

Then how to achieve a win-win?

Typescript got a Partial<T> utility type which simply makes all properties of the T optional. If we use Partial<Person> instead of Person as an argument type in personShortName() , then the previous example becomes valid:

function personShortName({name, surname}: Partial<Person>): string {*
    *return `${name[0]} ${surname}`;
}

️personShortName({ name: 'Robert', surname: 'Doe' });

But we have a side effect here, { name: string; surname; string } has became { name: string | undefined; surname: string | undefined; }.

Then, to prevent returning some weird results like “u. undefined”, we would need to perform some type checking inside the function, ensuring that each of the needed variables is defined.

function personShortName({name, surname}: Partial<Person>): string | null {*
    *return name && surname ? `${name[0]} ${surname}` : null;
}

And we've achieved… another side effect — now our function may return null, which may enforce some extra checking in a place of its usage.

But don't worry — there's a better way. TypeScript also provides another utility type — Pick<T, Keys> which selects some properties Keys from the T.

function personShortName({ name, surname }: Pick<Person, 'name' | 'surname'>): string  {
    return `${name[0]} ${surname}`;
}

This way we've achieved our goal — we've allowed calling this function both ways — passing a valid Person object or just the needed variables.

personShortName({ name: 'John', surname: 'Doe' });

const someone: Person = { name: 'John', ... }
personShortName(someone);

A step further

In the above solution, you haven't access to the rest of the Person properties inside the personShortName function. Let's assume you want to implement a function that uses name and surname only in the simplest variation, but you want to have access to the other Person properties too.

function personShortName(person: Person): string  {
  const { name, surname, uuid } = person;
  if (uuid) {
    const userRole = getUserRole(uuid);
    return `${name[0]} ${surname}, ${userRole}`;
  }

  /* some other operations on persons params */

  return `${name[0]} ${surname}`;
}

This kind of implementation won't allow you to use the simple call with name and surname directly. But we don't need all Person properties just to return the short name when there's no uuid. We need access to all Person properties, but only name and surname should be required. Maybe then let's allow person: Person | Pick_<_Person, 'name' | 'surname'_>_.

function personShortName(person: Person | Pick<Person, 'name' | 'surname'>)): string  {
  ❌ const { name, surname, uuid } = person;
  if (uuid) {
    const userRole = getUserRole(uuid);
    return `${name[0]} ${surname}, ${userRole}`;
  }

  /* some other operations on persons params */

  return `${name[0]} ${surname}`;
}

It's a good idea, but it still won't work. The compiler will say that property uuid don't exist on type { name: string, surname: string }.

The solution is the combination

This approach is a good direction because we've declared that we need a whole Person or { name, surname }. But in fact, we don't want either the whole Person or { name, surname }. The thing we want to accomplish is to have all the Person properties optional and name, surname to be required. We can accomplish that by combining the Partial<T> and Pick<T, Keys> utility types.

function personShortName(person: Partial<Person> & Pick<Person, 'name' | 'surname'>)): string  {
  const { name, surname, uuid } = person;
  if (uuid) {
    const userRole = getUserRole(uuid);
    userRole = getUserRole(uuid);
    return `${name[0]} ${surname}, ${userRole}`;
  }

  /* some other operations on persons params */

  return `${name[0]} ${surname}`;
}

The above syntax, Partial<Person> & Pick<Person, 'name' | 'surname'> gives us such type:

person: {
    uuid?: string;
    name: string; <- required
    surname: string; <- required
    sex?: string;
    height?: number;
    age?: number;
    pet?: Animal;
}

This way we can have some required properties and the others become optional. Hope you'll find it useful.

~ Dawid Witulski @ Evionica — 2021




Continue Learning