Efficient Null Handling in Typescript

null-handling-eye-catch JavaScript/TypeScript

Null or undefined handling appears many times. Do you have an interface with the nullable property but want to reuse it to create another data type?

Sponsored links

Non Null Assertion Operator (Exclamation mark operator)

Array.prototype.find() can return undefined so we need to check if it is not undefined. If you are sure that the result is not undefined you can use non-null assertion.

let array = [
    { key: "key1", value: 11 },
    { key: "key2", value: 22 },
    { key: "key3", value: 33 },
    { key: "key4", value: 44, special: { value: true } },
    { key: "key5", value: 55 },
];

const find = (val: number) => array.find((x) => x.value === val);
const result1 = find(22)
console.log(result1!.key);
// key2

But be careful to use it because it throws the following error when it is undefined.

TypeError: Cannot read property 'key' of undefined

Sponsored links

Optional chaining Operator (Question mark operator)

Optional chaining is safer than non-null assertion because it returns undefined when the variable is undefined. It doesn’t try to call chained property or function.

const result2 = find(2233);
console.log(result2?.key);
// undefined

const result3 = find(33);
console.log(result3?.key);
// key3

Optional chaining can of course be chained as many as we want.

let array = [
    { key: "key1", value: 11 },
    { key: "key2", value: 22 },
    { key: "key3", value: 33 },
    { key: "key4", value: 44, special: { value: true } },
    { key: "key5", value: 55 },
];
const existSpecial = (value: number) => {
    const found = find(value);
    if (found?.special?.value === true) {
        console.log(`special object found.`);
    } else {
        console.log(`special object not found.`);
    }
}
existSpecial(22);
// special object not found.
existSpecial(44);
// special object found.

Reuse an existing definition to create another type

Sometimes we want to create another interface that has the same properties as existing one but all properties are non null. Using extends keyword is one of the solutions for that but Typescript offers another way. Following is a base interface definition. We’ll use this to create other types.

interface MyType {
    name: string;
    age: number;
    hobby?: string;
}
const myType1: MyType = { name: "yu", age: 34 };
const myType2: MyType = { name: "yu", age: 34, hobby: "programming" };

Make all properties mandatory (Required)

We can reuse this definition to create another interface or type. Required makes all properties mandatory.

type AllPropsRequiredType = Required<MyType>;
const myType3: AllPropsRequiredType = { name: "yu", age: 34, hobby: "programming" };
const myType4: AllPropsRequiredType = { name: "yu", age: 34 }; // error
// Property 'hobby' is missing in type '{ name: string; age: number; }' but required in type 'Required<MyType>'.ts(2741)

Make all properties nullable (Partial)

In opposite to Required above, we can make all properties optional.

type PartialType = Partial<MyType>;
const partialType1: PartialType = {};

Omit unnecessary properties (Omit)

If the nullable property is unnecessary we can remove it from the definition.

type OmitType = Omit<MyType, "hobby">;
const omitType1: OmitType = { name: "yu", age: 34 };
const omitType2: OmitType = { name: "yu", age: 34, hobby: "error" }; // error
// Type '{ name: string; age: number; hobby: string; }' is not assignable to type 'OmitType'.
// Object literal may only specify known properties, and 'hobby' does not exist in type 'OmitType'.ts(2322)

Pick necessary properties (Pick)

In opposite to Omit above, we can pick only the necessary properties.

type PickType = Pick<MyType, "name" | "hobby">;
const pickType1: PickType = { name: "yu" };
const pickType2: PickType = { name: "yu", hobby: "programming" };

This post might be helpful for a nested object.

Change the type to Non Nullable (NonNullable)

If a type allows null and undefined but want to change it non-nullable type.

type NullableDataType = number | string | null | undefined;
const nullableData1: NullableDataType = null;
const nullableData2: NullableDataType = undefined;
const nullableData3: NullableDataType = "string";

type NonNullableType = NonNullable<NullableDataType>;
const nonNullableData1: NonNullableType = null; // error
// Type 'null' is not assignable to type 'NonNullableType'.ts(2322)
const nonNullableData2: NonNullableType = undefined; // error
// Type 'undefined' is not assignable to type 'NonNullableType'.ts(2322)
const nonNullableData3: NonNullableType = "string";

Set Optional or Mandatory

If you want to set some properties optional or mandatory, you can check this post too. In this post, Optional and Mandatory utilities are defined.

Null handling class to remove null check

We sometimes face a case that we need to put a null check in many places for the same object/class. Null check logic makes our code messy so it’s better to reduce it. Here is a small example.

interface Role {
    do(): void;
}
class Manager implements Role {
    do(): void {
        console.log("I make a plan.");
    }
}
class Developer implements Role {
    do(): void {
        console.log("I develop a software.");
    }
}
function roleFactory(value: string): Role | undefined {
    switch (value) {
        case "manager": return new Manager();
        case "developer": return new Developer();
        default: return undefined;
    }
}

const roles = ["manager", "developer", "sales"];
roles.forEach((role) => {
    const instance = roleFactory(role);
    if (instance) {
        instance.do();
    }
});

A class for “sales” is not defined so roleFactory returns undefined. We have to check if the instance is not undefined to call do function. It’s ok for this small example to check whether it’s undefined or not. However, what if there are other functions in the interface and roleFactory is called in different places? Null check logic appears in those places as well. Are there good ways for it?
Create a class to handle undefined/null and do nothing in it.

class UndefinedRole implements Role {
    do(): void { }
}

function roleFactory(value: string): Role {
    switch (value) {
        case "manager": return new Manager();
        case "developer": return new Developer();
        default: return new UndefinedRole();
    }
}

const roles = ["manager", "developer", "sales"];
roles.forEach((role) => {
    const instance = roleFactory(role);
    instance.do();
});

No check logic is needed in this way. Return an empty array if the return type is an array.

Comments

Copied title and URL