Borislav Hadzhiev
Wed Mar 09 2022·3 min read
Photo by Mael Balland
Use the logical OR operator to destructure from a nullable object in
TypeScript, e.g. const { name, age } = obj || ({} as Person)
. The logical OR
operator is used to provide a fallback in case the object has a value of null
or undefined
.
type Person = { name?: string; age?: number; }; const obj = null; const { name, age } = obj || ({} as Person); console.log(name); // 👉️ undefined console.log(age); // 👉️ undefined if (name !== undefined) { // 👇️ name is type string here console.log(name.toUpperCase()); }
The example shows how to destructure from a nullable object without using default values.
The obj
variable could store a null
or undefined
value, so we used the
logical OR (||)
operator to provide an empty object as a fallback.
However, TypeScript won't let us destructure properties from an empty object, so we had to use a type assertion to type the object.
undefined
value back.That's why we've typed the properties in the Person
type to be optional -
because of the possibility they will have undefined
values if we destructure
from an empty object.
An even better way would be to use the Partial utility type to achieve this result.
type Person = { name: string; age: number; }; const obj = null; // 👇️ using Partial<Type> const { name, age } = obj || ({} as Partial<Person>); console.log(name); // 👉️ undefined console.log(age); // 👉️ undefined if (name !== undefined) { // 👇️ name is type string here console.log(name.toUpperCase()); }
Partial
utility type constructs a new type with all properties of the type set to optional (could be undefined
).This way we are forced to use a
type guard
to check if the specific value is NOT undefined
before accessing built-in
methods on it, or assume it is of specific type.
The if
statement in the example serves as a type guard and checks if the value
is not undefined
.
Once we're in the if
block the name
variable is guaranteed to store a
string
.
You can also use default values when destructuring.
type Person = { name?: string; age?: number; }; const obj = null; // 👇️ using default values const { name = 'James', age = 30 } = obj || ({} as Person); console.log(name); // 👉️ "James" console.log(age); // 👉️ 30 console.log(name.toUpperCase()); // 👉️ "JAMES"
If your use case allows you to use default values, it is quite useful because
the type of the name
variable is guaranteed to be a string
, even though the
name
property is marked as optional in the Person
type.
string
.If the object you are destructuring from has nested properties, you have to use a default value of an empty object to be able to access them.
type Person = { name?: string; age?: number; address?: { country: string; }; }; const obj = null; const { // 👇️ default value of empty object (for nested properties) address: { country } = {}, name = 'James', age = 30, } = obj || ({} as Person); console.log(name); // 👉️ "James" console.log(age); // 👉️ 30 console.log(country); // 👉️ undefined if (country !== undefined) { console.log(country.toUpperCase()); }
The Person
type has an address
property that is an object with a country
property.
address
object.Now the type of the country
variable is string
or undefined
, so we have to
use a type guard to make sure it's a string
.
Alternatively, you could provide a default value for the country
variable as
well.
type Person = { name?: string; age?: number; address?: { country: string; }; }; const obj = null; const { address: { country = '' } = {}, name = 'James', age = 30, } = obj || ({} as Person); console.log(name); // 👉️ "James" console.log(age); // 👉️ 30 console.log(country.toUpperCase()); // 👉️ ""
We passed an empty string as the default value for the country
variable. The
benefit of doing this is that it is now guaranteed to be a string
, so no type
guards are needed.