Idiomatic Programming

31. March, 2023 5 min read Teach

Discovering Idiomatic Patterns

Idiomatic programming refers to writing code that follows a language's conventions and best practices.

Idiomatic code is typically more readable, maintainable, and performant. I’ll be focusing on these patterns specifically for TypeScript, but most of the practices can also be applied to other languages.

Using Interfaces

TypeScript provides interfaces, one of the most potent idiomatic patterns, which allow us to define custom types for our code. For example:

// define an interface
interface User {
  name: string;
  age: number;
  email: string;
}

// apply it to our arguments
const sendEmail: void = (user: User, message: string) => {
  // do stuff
}

// Call the function with a User object
const user: User = {
  name: 'John',
  age: 30,
  email: 'john@wick.com',
};

sendEmail(user, 'Hello!');

Using Type Inference

Another option is type inference. In many cases, that approach allows us to omit explicit type annotations, as the compiler can infer the types based on the context. This approach can make our code more concise and readable, as we don’t have to clutter our code with unnecessary type information. For example:

// type string is automatically inferred
const name = 'John';

// the return value is automatically inferred
const add = (a: number, b: number) => {
  return a + b;
}

const result = add(2, 3);

Using Promises

Asynchronous code is a common pattern in modern web development, and TypeScript provides built-in support for promises, a standardized way to handle asynchronous operations. Using promises for asynchronous code is an idiomatic way to ensure our code is readable, maintainable, and scalable. For example:

// use a Promise to fetch data from an API
const fetchData: Promise<any> = () => {
  return fetch('/api/data')
    .then(response => response.json())
    .catch(error => console.error(error));
}

// call the fetchData function and handle the Promise
fetchData()
  .then(data => console.log(data))
  .catch(error => console.error(error));

Using the readonly Modifier

TypeScript provides the readonly modifier, which allows us to create read-only properties on an object. Using readonly properties can make our code more secure and less prone to errors, as it prevents accidental modification of important data. For example:

// define a read-only property on an object
interface Person {
  readonly name: string;
  age: number;
}

// attempt to modify the read-only property
const person: Person = { name: 'John', age: 30 };
person.name = 'Bob'; // ERROR: Cannot assign to 'name' because it is a read-only property

Using Enums

The language also provides enums, which allow us to define a set of named constants. Using enums for constants can make our code more readable and self-documenting, as it clearly communicates a variable’s or parameter’s intended values. For example:

// define an enum for colours
enum Color {
  RED = 'red',
  GREEN = 'green',
  BLUE = 'blue',
}

// use the enum
const setBackgroundColor = (color: Color) => {
  document.body.style.backgroundColor = color;
}

// call the function with an enum value
setBackgroundColor(Color.BLUE);

Using Generics

It also provides generics, allowing us to create reusable code that can work with various types. Using generics for reusable code can make our code more flexible and efficient, as it allows us to avoid duplicating code for similar but slightly different types. For example:

// define a generic function to return the first element of an array
function getFirst<T>(array: T[]): T | undefined {
  return array[0];
}

// call the function with different types of arrays
const numbers = [1, 2, 3];
const firstNumber = getFirst(numbers); // Type is number

const strings = ['hello', 'world'];
const firstString = getFirst(strings); // Type is string

Other Patterns

Many other idiomatic patterns and best practices in JavaScript/TypeScript can help us write more effective code. Here are a few more examples:

  • Using Type Aliases and Interfaces: Creating reusable type definitions for complex types and data structures.
  • Using Union and Intersection Types: Creating flexible and reusable types that can represent multiple values or combinations of types.
  • Using Type Guards and Assertions: Checking the type of a value at runtime and narrowing its type accordingly, or asserting that a value is of a specific type.
  • Using Generics: Creating reusable and type-safe functions and classes that can work with a variety of types.
  • Using Decorators: Adding behavior or metadata to classes and their members at design time, and reflecting on it at runtime.
  • Using Namespaces and Modules: Organizing code into logical units that can be easily shared and reused.
  • Using Tuples: Defining fixed-length arrays with specific types for each element.
  • Using Type Predicates: Creating functions that return a boolean value indicating whether a value is of a specific type.
  • Using Nullish Coalescing and Optional Chaining: Writing more concise and safe code that handles null and undefined values.
  • Using Destructuring: Extracting values from arrays and objects into separate variables, reducing code repetition and improving readability.
  • Using Currying: Creating higher-order functions that can be partially applied to create new functions with fewer arguments.
  • Using the never Type for Exhaustiveness Checking: Creating robust code that catches all possible cases in a switch statement or other control flow.
  • Using the keyof Operator for Type-Safe Access to Object Properties: Creating flexible and reusable code that can work with a variety of objects by using type-safe access to object properties.
  • Using the unknown Type for Dynamic Data Validation: Creating more robust code that validates and processes dynamic data using type guards and assertions.

Summary

Idiomatic programming is an important concept to remember when writing code, as it can help us create more efficient, maintainable, and scalable applications. By following any language’s conventions and best practices, we can ensure our code is readable and consistent. This understanding helps avoid common pitfalls and errors arising from deviating from these standards.

‘Till next time!