Leverage TypeScript's Utility Types for Enhanced Efficiency and Code Reusability
TypeScript Banner

Leverage TypeScript's Utility Types for Enhanced Efficiency and Code Reusability

When we make the leap from JavaScript to TypeScript, we encounter a new world of possibilities to enhance the quality of our code.

However, one of the biggest challenges we face is the need to assign types to everything, which can be overwhelming. This is where TypeScript's “Utility Types” come into play, a powerful tool that allows us to optimize and reuse our code efficiently.

Utility Types are a set of predefined types in TypeScript that offer advanced functionalities to manipulate and transform existing types. With them, we can avoid incorrect practices such as excessive use of any, creating interfaces or types with unnecessary optional properties, or code duplication. Instead, we can make the most of utility types to simplify our syntax, improve readability, and ensure greater security in the development of our applications.

In this article, we will explore the key benefits of utility types in TypeScript and how they can empower our types. We will look at common use cases, such as accessing specific property types using “Index Access Type”, creating optional types with “Partial”, and selectively excluding properties with “Omit”. We will also discover how to combine these utility types to achieve more concise and maintainable code.

By mastering utility types in TypeScript, we will be able to optimize our way of working, reduce code duplication, and make our code more readable and scalable. Remember that in software development, every small improvement matters, and utility types are a powerful tool to optimize and reuse our code.

? Case 1: Excessive Use of any (An Unfavorable Approach in TypeScript)

In this first case, we encounter a common problem when migrating from JavaScript to TypeScript: the temptation to use the “any" type in a generalized manner. This approach, although it may seem tempting to avoid type errors, is counterproductive and goes against the principles of TypeScript.

function getOne(id: any) {
  // code
}

function getAll() {
  // code
}

function createOne(data: any) {
  // code
}

function updateOne(id: any, newData: any) {
  // code
}

function deleteOne(id: any) {
  // code
}
        

? Case 2: Using Interfaces or Types with All Optional Values (False positives)

In the second case, we explore another practice that is not recommended: using interfaces or types with all properties marked as optional. While this may eliminate type errors initially, it leads to less secure code and makes it harder to understand and maintain.

interface Task {
  id?: string;
  title?: string;
  description?: string;
  priority?: number;
  author?: string;
}

function getOne(id: string): Task {
  // code
}

function getAll(): Array<Task> {
  // code
}

function createOne(data: Task) {
  // code
}

// THIS SHOULD RAISE A TYPE ERROR BUT IT DOESN'T WITH THIS CASE
createOne({
  title: '',
});

function updateOne(_id: string, newData: Task) {
  // code
}

function deleteOne(id: string) {
  // code
}
        

? Case 3: Creating Multiple Interfaces and Types (Code Duplication)

In the third case, we encounter a common situation where multiple interfaces or types with repeated properties are created. While this may solve the initial problem, it results in unnecessary code duplication and hinders the evolution and refactoring of the code.

// All properties of this interface are repeated
interface GetOneTask {
  id: string;
  title: string;
  description?: string;
  priority: number;
  author: string;
}

type GetAllTask = Array<GetOneTask>;

// All properties (except id) of this interface are repeated
interface CreateOneTask {
  title: string;
  description?: string;
  priority: number;
  author: string;
}

// All properties (except id) of this interface are repeated, the only difference from CreateOneTask is that all properties are optional
interface UpdateOneTask {
  title?: string;
  description?: string;
  priority?: number;
  author?: string;
}

function getOne(id: string): GetOneTask {
  // code
}

function getAll(): GetAllTask {
  // code
}

function createOne(data: CreateOneTask): GetOneTask {
  // code
}

// SHOULD RAISE AN ERROR AND IT DOES IT
createOne({
  title: '',
});

function updateOne(_id: string, newData: UpdateOneTask): GetOneTask {
  // code
}

function deleteOne(id: string): void {
  // code
}
        

? Case 4: Utility Types (The Good Approach)

In contrast to the previous cases, in this final case, we explore an optimal solution using TypeScript's utility types. These utility types allow us to optimize and reuse our code efficiently, avoiding incorrect practices and improving the readability and maintainability of the code.

interface Task {
    id: string;
    title: string;
    priority: number;
    author: string;
}

function getOne(id: Task['id']): Task {
    // code
}

function getAll(): Array<Task> {
    // code
}

function createOne(data: Omit<Task, 'id'>) {
    // code
}

function updateOne(id: Task['id'], newData: Omit<Partial<Task>, 'id'>) {
    // code
}

function deleteOne(id: Task['id']) {
    // code
}
        

In this case, we are defining a base interface that has all the fields of the “Task” entity as it would be stored in the database. After this, we can modify its behavior according to the method using utility types.

Index Access Type: Access Specific Property Types

Although this is not a utility type per se, this is a very useful feature that allows us to obtain the type of property by accessing it using square brackets ([]). For example, if we have the following type:

interface Task {
    id: string;
    title: string;
    priority: number;
    author: string;
}
        

We can access the type of the “id” property as follows:

Task['id'] // string        

In this way, we can use the “Index Access Type” to obtain specific property types and use them in variables, functions, or even create new types based on them.

const taskId: Task['id'] = '' // string
function getTask(id: Task['id']): Task // (id: string): Task
type TaskId = Task['id'] // string
        

The advantages of using the “Indexed Access Type” allow us to make changes more efficiently and consistently in our code, as we only need to make modifications in one place. The rest of the code using the type will automatically access the new definition.

If you want more information about this TypeScript feature, I recommend checking the official TypeScript documentation on “Indexed Access Types” at Indexed Access Types.

Partial (Utility Type): Make All Properties Optional

The “Partial” utility type is a powerful tool that allows us to create a new type or interface where all properties are optional. This provides us with flexibility and saves us from manually marking each property as optional. In the example, we have the “Task” interface with the following properties:

interface Task {
    id: string;
    title: string;
    priority: number;
    author: string;
}
        

If we want to make all properties of the object optional, we can use "Partial" as follows:

type OptionalTask = Partial<Task>        

In this case, "OptionalTask" is defined as a new type or interface where all properties of "Task" become optional. Therefore, "OptionalTask" will have the following structure:

interface OptionalTask {
    id: string | undefined;
    title: string | undefined;
    priority: number | undefined;
    author: string | undefined;
}
        

Each property in "OptionalTask" can now be "undefined" in addition to having its original type. This gives us the flexibility to assign or not assign values to those properties in objects that comply with "OptionalTask."

Omit (Utility Type): Exclude Specific Properties

Another useful utility type is "Omit," which allows us to create a new type or interface by excluding specific properties from an existing type. This helps us eliminate unwanted properties and simplify the structure of our types. For example, if we have the base interface Task with the following properties:

// Base Interface
interface Task {
    id: string;
    title: string;
    priority: number;
    author: string;
}
        

If we want to create a new type or interface from "Task" that excludes the "id" property, we can use "Omit" as follows:

type TaskWithoutId = Omit<Task, 'id'>        

In this case, "TaskWithoutId" is defined as a new type or interface that has all properties of Task except the "id" property. Therefore, "TaskWithoutId" will have the following structure:

interface TaskWithoutId {
    title: string;
    priority: number;
    author: string;
}
        

We can see that the "id" property has been excluded in "TaskWithoutId".

Additionally, it is possible to use "Omit" to exclude multiple properties at once. For example, if we want to exclude both the id and author properties, we can do so using the union operator (|):

type TaskWithoutIdAndAuthor = Omit<Task, 'id' | 'author'>        

In this case, "TaskWithoutIdAndAuthor" is defined as a new type or interface that has all properties of "Task" except the "id" and author properties. Therefore, "TaskWithoutIdAndAuthor" will have the following structure:

interface TaskWithoutIdAndAuthor {
    title: string;
    priority: number;
}
        

We can see that both the "id" and "author" properties have been excluded in "TaskWithoutIdAndAuthor".

Using "Omit" is useful when we want to create new types or interfaces based on an existing one but excluding certain properties to fit specific needs of our code. This allows us to have greater flexibility and reusability in TypeScript application development.

Combining Utility Types: Empower Your Types??

An additional advantage of utility types is that they can be combined to achieve more powerful and specific results. We can use multiple utility types together to tailor our types to different scenarios and requirements. Given the "Task" interface with the following properties:

interface Task {
    id: string;
    title: string;
    priority: number;
    author: string;
}
        

If we want to create an update function (updateOne) that recives an "id" and the new data (newData) to update a task, we can use the "Partial" and "Omit" utility types as follows:

function updateOne(id: Task['id'], newData: Omit<Partial<Task>, 'id'>) {
    // code
}
        

In this case, we are using "Partial<Task>" to make all properties of "Task" optional, resulting in the following type or interface:

interface OptionalTask {
    id: string | undefined;
    title: string | undefined;
    priority: number | undefined;
    author: string | undefined;
}
        

Then, we pass the result of "Partial" to "Omit" to exclude the id property. This gives us a new type or interface where all properties are optional, the "id" property does not exist. Therefore, the result of "Omit<Partial<Task>, 'id'>" is:

interface OptionalTask {
    title: string | undefined;
    priority: number | undefined;
    author: string | undefined;
}
        

This combination of utility types allows us to define the type of the parameters of the "updateOne" function precisely, indicating that the identifier (id) should be of type "Task['id']" and the new data (newData) should have all properties of "Task" except "id" and be optional.

Additionally, we can use this combination of utility types in other situations, such as typing variables, functions, or even creating a new type based on this pattern.

Examples of use would be:

const updateTask: Omit<Partial<Task>, 'id'> = {}
function updateTaskFn(task: Omit<Partial<Task>, 'id'>): void
type UpdateTask = Omit<Partial<Task>, 'id'>
        

These examples demonstrate the versatility of utility types in TypeScript and how we can leverage their combination to achieve greater flexibility in handling types and data structures.

Conclusion

By using utility types in TypeScript, we can achieve a significant reduction in the amount of code needed to accomplish certain goals. In the example presented, we were able to reduce the code from 48 lines to 26, eliminating code repetition and leveraging the available utility types.

Utility types are a fundamental feature of TypeScript and will become a common tool in your developer toolbox. These types offer a wide range of functionalities and possibilities that simplify and enhance type handling in TypeScript.

It is important to note that the example presented in this article only scratches the surface of what utility types can do. There are many more utility types available, which you can explore in the official TypeScript documentation. There, you will find a complete list of built-in utility types in TypeScript, along with examples and use cases.

By mastering the use of utility types, you can improve the readability, maintainability, and scalability of your code, while reducing the likelihood of making errors. These tools will enable you to develop more robust and efficient applications in TypeScript. Don't hesitate to make the most of utility types to empower your projects!

References


要查看或添加评论,请登录

社区洞察

其他会员也浏览了