dry-principles-in-typescript
DRY Principles in TypeScript:
Understanding DRY:
DRY stands for "Don't Repeat Yourself". It's a principle aimed at reducing the repetition of software patterns. In TypeScript, it means creating a codebase where each piece of knowledge or logic is only represented once.
Functions for Reusable Logic:
Use functions to encapsulate reusable logic. If you find yourself writing the same code multiple times, it's often a sign that you should extract it into a function.
Generic Types for Flexibility:
Utilize generic types to create flexible and reusable components or functions that can work with any type, avoiding duplication for each type you work with.
Interfaces for Object Shapes:
Define interfaces to represent the shape of objects. This way, you avoid repeating the same type annotations and can easily change the shape of related objects by changing just the interface.
Utility Types to Manipulate Types:
TypeScript comes with built-in utility types such as
Partial
,Readonly
, andRecord
that help you manipulate types in a DRY way, without having to redefine new types from scratch.Extending Interfaces and Classes:
Extend existing interfaces and classes to create new specifications. This keeps your code DRY by allowing you to build on what is already defined.
Module Imports to Share Code:
Use modules to share and reuse code across different parts of your application. Importing modules can help keep your codebase DRY by having a single source of truth.
Type Aliases for Complex Types:
Create type aliases for complex or commonly used types. Instead of repeating complex type annotations, you can define them once and then use the alias.
Enums for Fixed Value Sets:
Use enums to encapsulate a set of fixed values. This avoids repeating the same set of literal values and makes it easier to update them if needed.
Type Aliases for Complex Types:
Create type aliases for complex or commonly used types. Instead of repeating complex type annotations, you can define them once and then use the alias.
// Bad: Repeated complex type
function processUser(user: { name: string; age: number; email: string }) {
// ...
}
// Good: Type alias
type User = { name: string; age: number; email: string };
function processUser(user: User) {
// ...
}
- Enums for Fixed Value Sets:
Use enums to encapsulate a set of fixed values. This avoids repeating the same set of literal values and makes it easier to update them if needed.
// Bad: Repeated literals
const STATUS_ACTIVE = 'active';
const STATUS_INACTIVE = 'inactive';
const STATUS_PENDING = 'pending';
// Good: Enum
enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending'
}
- Function Overloading for Variation:
Instead of writing multiple functions that do similar things, use function overloading to handle different types or numbers of arguments.
// Bad: Multiple functions for each variation
function greet(name: string) {
console.log(`Hello, ${name}!`);
}
function greetWithAge(name: string, age: number) {
console.log(`Hello, ${name}! You are ${age} years old.`);
}
// Good: Function overloading
function greet(name: string, age?: number) {
if (age !== undefined) {
console.log(`Hello, ${name}! You are ${age} years old.`);
} else {
console.log(`Hello, ${name}!`);
}
}
- Mapped Types for Creating Variants:
Use mapped types to create new types by transforming properties of existing ones. This helps in keeping your type definitions DRY.
// Bad: Manually creating a variant
interface ReadOnlyUser {
readonly name: string;
readonly age: number;
}
// Good: Mapped type
type ReadOnly<T> = { readonly [P in keyof T]: T[P] };
type ReadOnlyUser = ReadOnly<User>;
- Decorator Functions for Meta-programming:
Decorators allow you to annotate and modify classes and properties at design time. They can be used to add common behavior to various parts of your application in a DRY manner.
// A simple log decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Arguments: ${args}`);
return originalMethod.apply(this, args);
}
}
class MathOperations {
@log
add(x: number, y: number) {
return x + y;
}
}
- Using Mixins for Composition:
Mixins are a way of composing classes from multiple sources. Create classes that can be combined to add common functionality instead of repeating the same methods across classes.
// Mixin function
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
class Disposable {
dispose() {
console.log('Disposing resource');
}
}
class Activatable {
activate() {
console.log('Activating');
}
}
// Combined class with mixins
class SmartObject implements Disposable, Activatable {
// Mixin properties will go here
dispose: () => void;
activate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);
- Conditional Types for Type Logic:
Leverage conditional types to avoid repeating type definitions. They allow you to branch type logic based on conditions, creating more dynamic and reusable type definitions.
// Conditional type that returns a string or number type based on the input type
type StringOrNumber<T> = T extends boolean ? string : number;
// Usage
let a: StringOrNumber<true>; // Type is string
let b: StringOrNumber<false>; // Type is number
- DRY in Testing with Utility Functions:
When writing tests in TypeScript, create utility functions for common setup and teardown steps. This approach keeps your tests DRY, easier to read, and maintain.
// Bad: Repetitive setup in tests
it('test 1', () => {
const user = new User('John', 'Doe');
// test logic
});
it('test 2', () => {
const user = new User('John', 'Doe');
// test logic
});
// Good: Utility function for creating test users
function createTestUser() {
return new User('John', 'Doe');
}
it('test 1', () => {
const user = createTestUser();
// test logic
});
it('test 2', () => {
const user = createTestUser();
// test logic
});
By using these strategies, you can ensure that your TypeScript code follows the DRY principle, which can significantly improve code quality and reduce the likelihood of bugs.