How to simplify TypeScript types and reduce documentation overhead
When working with TypeScript, it's important to strike a balance between type safety and code readability. Type definitions can become verbose and require extensive documentation to convey their purpose. In this article, we'll explore a technique to reduce the need for excessive documentation by using unions in TypeScript.
Consider the following code snippet:
type Task = {
status: string;
name: string;
description?: string;
at?: string;
expectedDone?: Date;
doneOn?: Date;
};
const myTask: Task = {
name: 'Clean the house',
description: 'Clean the house before 5pm',
at: 'Home',
status: 'inProgress',
expectedDone: new Date('2021-01-01')
}
The Task
type defined above captures various properties related to a task, such as its status
, name
, description
, and timestamps. While this definition is straightforward, it lacks clarity and requires additional documentation to explain its use.
We can use union types to simplify the Task
type and eliminate the need for excessive documentation. Unions allow us to create type variants based on a shared discriminant property. In this case, the discriminant property is the state field.
Let's refactor the code:
type Task = {
name: string
description?: string
at: string
} & ({
status: 'toDo'
} | {
status: 'inProgress'
expectedDone: Date
} | {
status: 'done'
expectedDone: Date
doneOn: Date
})
const myTask: Task = {
name: 'Clean the house',
description: 'Clean the house before 5pm',
at: 'Home',
status: 'inProgress',
expectedDone: new Date('2021-01-01')
}
In the refactored code, the Task
type is defined as a union of three different types, each representing a different task state. By using the discriminant union, TypeScript can narrow down the possible types based on the status property, allowing for precise type checking and inference.
We've also eliminated redundant properties from each type variant. The name
, description
, and at
properties are common to all variants, so we define them once in the junction type (&
) following each type variant.
With the refactored Task
type, it's clear that the status property can only have one of the three specified values: toDo
, inProgress
, or done
. TypeScript's type inference mechanism automatically guides us to provide the appropriate properties based on the chosen status value.
We've achieved a more concise and self-explanatory code structure by using discriminated unions and intersection types. This approach reduces the need for excessive documentation, as the type definitions communicate the possible variations and constraints of the Task type.
In conclusion, when working with complex type definitions in TypeScript, consider using discriminated unions and intersection types to simplify your code and reduce the need for extensive documentation. By taking advantage of these powerful language features, you can improve the readability and maintainability of your code while ensuring type safety in your projects.