Introduction
In this article, we discuss object type transformations in TypeScript using Partial<>
. This is the third part of the series titled TypeScript Utility Type Series.
In the previous post, we went through an example where we derived a Subscriber
type by omitting a property from the base type, SuperbUser
, with Omit<>
.
In this post, we will consider an example of TypeScript Partial<>
by modifying our Subscriber
type to assume a more realistic scenario.
Steps we'll cover:
Optional Registration Scenario
For our blog, we would have GuestUser
s who are not allowed to like or comment on a post. We would allow registered Subscriber
s to like and comment. So, the scenario goes:
- a
GuestUser
must register with theiremail
and become aSubscriber
. - they receive a link to set their password in an email sent to their
email
. - they are able to set their
password
,firstName
andlastName
afterwards.
In such a scenario, the Subscriber
type we derived previously, which effectively has the shape below, does not deliver our needs the way we want:
type Subscriber = {
userId: number,
macAddress: string,
username: string,
email: string,
password: string,
firstName: string,
lastName: string,
};
const subscriber: Subscriber = {
userId: 4,
macAddress: 'a:5ub:mach1ne',
username: 'sub_user',
email: 'sub_user@gmail.com'
};
console.log(subscriber);
/*
From TypeScript Error:
"Type '{ userId: number; macAddress: string; username: string; email: string; }' is missing the following properties from type 'Subscriber': password, firstName, lastName"
From console:
{
"userId": 4,
"macAddress": "a:5ub:mach1ne",
"username": "sub_user",
"email": "sub_user@gmail.com"
}
*/
TypeScript complains about the inconformity of subscriber
to Subscriber
type. This is because it sets all the fields to be required by default. If we set password
, firstName
and lastName
to optional manually, it is happy:
type Subscriber = {
userId: number,
macAddress: string,
username: string,
email: string,
password?: string,
firstName?: string,
lastName?: string,
};
const subscriber: Subscriber = {
userId: 4,
macAddress: 'a:5ub:mach1ne',
username: 'sub_user',
email: 'sub_user@gmail.com'
};
console.log(subscriber);
/*
{
"userId": 4,
"macAddress": "a:5ub:mach1ne",
"username": "sub_user",
"email": "sub_user@gmail.com"
}
*/
But this comes with the overhead of defining Subscriber
manually in the first place and additionally then setting individual optional properties. In real APIs, it's not a good idea to define a shape manually.
Enter TypeScript Partial<Type>
We want to remove the hassle and do this much more comfortably from the type returned from Omit<>
. So what we want to do is set all the properties of the returned type to be optional with Partial<Type>
:
type SuperbUser = {
userId: number,
macAddress: string,
username: string,
email: string,
password: string,
firstName: string,
lastName: string,
roles: ('Admin' | 'Editor' | 'Author')[]
};
type Subscriber = Partial<Omit<SuperbUser, 'roles'>>;
const subscriber: Subscriber = {
userId: 4,
macAddress: 'a:5ub:mach1ne',
username: 'sub_user',
email: 'sub_user@gmail.com'
};
console.log(subscriber);
/*
{
"userId": 4,
"macAddress": "a:5ub:mach1ne",
"username": "sub_user",
"email": "sub_user@gmail.com"
}
*/
No complains, which is great!
So, we are now free to set values for password
, firstName
and lastName
:
subscriber.password = '12345678';
subscriber.firstName = 'Abdullah';
subscriber.lastName = 'Numan';
console.log(subscriber);
/*
{
"userId": 4,
"macAddress": "a:5ub:mach1ne",
"username": "sub_user",
"email": "sub_user@gmail.com",
"password": "12345678",
"firstName": "Abdullah",
"lastName": "Numan"
}
*/
But, like before, TypeScript complains again if we add the properties that disrupts the shape of Subscriber
:
subscriber.roles = ['Reader', 'Commenter'];
console.log(subscriber);
// Property 'roles' does not exist on type 'Partial<Omit<SuperbUser, "roles">>'.
So, the benefits of using TypeScript to derive a partial type includes its support for partial assignment of the object's properties that is allowed by JavaScript and not allowed by default TypeScript. It also warns about possible undesired assignments to the partial.
With Interfaces
We get the same result if we use an interface for our base SuperbUser
type:
interface SuperbUser {
userId: number;
macAddress: string;
username: string;
email: string;
password: string;
firstName: string;
lastName: string;
roles: ('Admin' | 'Editor' | 'Author')[]
};
type Subscriber = Partial<Omit<SuperbUser, 'roles'>>;
const subscriber: Subscriber = {
userId: 4,
macAddress: 'a:5ub:mach1ne',
username: 'sub_user',
email: 'sub_user@gmail.com'
};
subscriber.password = '12345678';
subscriber.firstName = 'Abdullah';
subscriber.lastName = 'Numan';
console.log(subscriber);
/*
{
"userId": 4,
"macAddress": "a:5ub:mach1ne",
"username": "sub_user",
"email": "sub_user@gmail.com",
"password": "12345678",
"firstName": "Abdullah",
"lastName": "Numan"
}
*/
We can also refactor the return type from Omit<>
into an intermediary type, StrictSubscriber
, and pass it as the argument of TypeScript Partial:
type StrictSubscriber = Omit<SuperbUser, 'roles'>;
type Subscriber = Partial<StrictSubscriber>;
Conclusion
In this post, we covered partial object transformations with using the utility transformer Partial. We found out that it is preferable to setting certain properties of a type to optional manually, especially when dealing with types returned from APIs.