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