The Problem with Empty Arrays
When working with arrays in TypeScript, we can specify the types, for example, an array of numbers number[]
. But this can lead to errors because we can create an empty array even if we say it should be an array of numbers.
const numbersArray: number[] = []
const getFirstValue = (array: number[]) => array[0];
console.log(getFirstValue(numbersArray)); // undefiend, no type error
Introducing Non-Empty Arrays
TypeScript gives us the ability to create custom generic types and type helpers, and this can be used to create a specific constraint: a "Non-empty array" type.
type NonEmptyArray<T> = [T, ...T[]]
Here we create a generic type helper that ensures that the array contains at least one element of the given type T, but it could also include more. This type is defined using a combination of [T] and ...T[], forming a variadic tuple.
- [T] signifies that there must be exactly one element of type T at the beginning of the array, guaranteeing that the array is never empty.
- ...T[] represents the rest of the elements in the array, allowing for zero or more additional elements of type T. This part ensures flexibility in the number of elements while maintaining type safety.
This custom type can be a valuable tool in scenarios where an empty array could lead to unexpected behaviors or errors. By enforcing the presence of at least one element, this type helps improve the robustness of the code and provides clear documentation of the array's expected content.
Going back to our array of numbers, we can improve it using our new type:
const numbersArray1: NonEmptyArray<number> = [] // Type error
const numbersArray2: NonEmptyArray<number> = [1] // Valid
const getFirstValueSafe = (array: NonEmptyArray<number>) => array[0];
const numbersArray3: number[] = []
console.log(getFirstValueSafe(numbersArray2)); // 1
console.log(getFirstValueSafe(numbersArray3)); // Type error
Applying Non-Empty Arrays to Real-World Problems
When working with users in a system, often they need to have specific roles. Let's look at a common example where a user can have a name and an array of different roles.
type Roles = "admin" | "editor" | "user";
interface UserUnsafe {
name: string;
roles: Roles[];
}
const createUserWithRoles = (name: string, roles: Roles[]) => ({
name,
roles
});
// We can create a user with empty roles that can lead to bugs in our code.
const newUnsafeUser = createUserWithRoles("Bob", []);
Instead, we should use our Non-Empty Array type:
interface UserSafe {
name: string;
roles: NonEmptyArray<Roles>;
}
const createUserWithRolesSafe = (name: string, roles: NonEmptyArray<Roles>) => ({
name,
roles
});
const newSafeUser1 = createUserWithRolesSafe("John", []); // Type Error
const newSafeUser2 = createUserWithRolesSafe("John", ["admin"]);
Conclusion
The Non-Empty Array type in TypeScript provides an elegant way to enforce constraints on array content. It allows for better control, reducing the chances of unexpected errors related to empty arrays. By leveraging the power of generics and tuple types, you can create more robust and readable code.
Top comments (0)