Idiomatic TypeScript (TS) code refers to writing TypeScript code in a way that follows best practices and conventions and is considered to be the most natural, efficient, and effective way to write code in TypeScript.

How modern idiomatic TS code for back-end looks like?

Modern idiomatic TypeScript (TS) code for back-end development typically focuses on clean code architecture, scalability, and maintainability. Here are some characteristics that you might find in modern TS code for back-end development:

  1. Use of TypeScript features:
    The codebase is written in TypeScript, which provides features such as static typing, interfaces, classes, and generics to make the code more maintainable and easier to refactor.
  2. Use of modern frameworks and libraries:
    Popular modern frameworks and libraries, such as Express, NestJS, TypeORM, and Prisma, are commonly used in the development of modern TS back-end code.
  3. Use of functional programming principles:
    Functional programming principles such as immutability, pure functions, and higher-order functions are used to write clean and concise code.
  4. Use of async/await and Promises:
    Asynchronous programming is key in modern back-end development, and async/await and Promises are widely used to manage asynchronous operations.
  5. Use of dependency injection:
    Dependency injection is used to make the code more testable and modular. Popular dependency injection frameworks in the TS ecosystem include InversifyJS and NestJS.
  6. Use of design patterns:
    Common design patterns such as Singleton, Factory, and Repository are used to make the code more maintainable and easier to reason about.
  7. Use of linting and formatting tools:
    Linting tools such as ESLint and formatting tools such as Prettier are used to enforce code consistency and improve code readability.

Overall, modern idiomatic TS code for back-end development aims to be maintainable, scalable, and easy to reason about by following best practices and leveraging the features provided by the TS language and its ecosystem.

Find below some great examples of idiomatic (backend) TS code.

 

Idiomatic (backend) TS code examples compared to JavaScript

 

1. Using interfaces for type definitions:

 

TypeScript

interface User {

  id: number;

  name: string;

  email: string;

}




function getUser(id: number): User {

  // fetch user from database

  const user: User = { id: 1, name: 'John Doe', email: '[email protected]' };

  return user;

}

 

JavaScript

function getUser(id) {

  // fetch user from database

  const user = { id: 1, name: 'John Doe', email: '[email protected]' };

  return user;

}

Note that in JavaScript, there is no need to specify the type of the function parameter or the return value. The interface User’ declaration is also not necessary, as JavaScript does not have interfaces.

 

2. Using async/await for asynchronous operations:

 

TypeScript

async function updateUser(id: number, name: string): Promise<User> {

  const user = await User.findOne(id);

  if (!user) throw new Error('User not found');

  user.name = name;

  await user.save();

  return user;

}

 

JavaScript

function updateUser(id, name) {

  return User.findOne(id)

    .then(function(user) {

      if (!user) throw new Error('User not found');

      user.name = name;

      return user.save();

    });

}

Note that in JavaScript, there is no need to specify the return type of the function using the Promise<User> syntax. Instead, we can simply return the Promise object that is created when calling User.findOne(id), and chain the then() method to handle the returned user object. Also, the async/await syntax in TypeScript can be replaced with the use of Promises in JavaScript.

 

3. Using dependency injection for modularity:

 

TypeScript

interface UserRepository {

  find(id: number): Promise<User>;

  save(user: User): Promise<void>;

}




class UserService {

  constructor(private readonly userRepository: UserRepository) {}




  async getUser(id: number): Promise<User> {

    const user = await this.userRepository.find(id);

    if (!user) throw new Error('User not found');

    return user;

  }




  async updateUser(id: number, name: string): Promise<User> {

    const user = await this.userRepository.find(id);

    if (!user) throw new Error('User not found');

    user.name = name;

    await this.userRepository.save(user);

    return user;

  }

}

JavaScript

class UserService {

  constructor(userRepository) {

    this.userRepository = userRepository;

  }




  async getUser(id) {

    const user = await this.userRepository.find(id);

    if (!user) throw new Error('User not found');

    return user;

  }




  async updateUser(id, name) {

    const user = await this.userRepository.find(id);

    if (!user) throw new Error('User not found');

    user.name = name;

    await this.userRepository.save(user);

    return user;

  }

}

 

Note that interfaces are not present in JavaScript, so we can omit the UserRepository interface. In the UserService constructor, we declare the userRepository parameter using the private readonly syntax, which is a TypeScript feature. In JavaScript, we can simply declare it as a normal parameter. The rest of the code can be used as-is since it is valid JavaScript syntax.

 

4. Using design patterns for maintainability:

 

TypeScript

class Database {

  private static instance: Database;




  private constructor() {}




  static getInstance(): Database {

    if (!Database.instance) {

      Database.instance = new Database();

    }

    return Database.instance;

  }




  // ...

}

 

JavaScript

class Database {

  constructor() {}




  static getInstance() {

    if (!Database.instance) {

      Database.instance = new Database();

    }

    return Database.instance;

  }




  // ...

}




Database.instance = null;

 

In this case, the only TypeScript feature used is the private constructor and private static instance variable. We can convert these to plain JavaScript by simply removing the private keyword, and initializing the instance variable outside the class. The getInstance method remains the same.

 

5. Using linting and formatting tools for code consistency:

 

TypeScript

// .eslintrc.js

module.exports = {

  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],

  parser: '@typescript-eslint/parser',

  plugins: ['@typescript-eslint'],

  rules: {

    // ...

  },

};




// .prettierrc.js

module.exports = {

  trailingComma: 'es6',

  tabWidth: 2,

  semi: true,

  singleQuote: true,

};

 

JavaScript

module.exports = {

  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],

  parser: '@typescript-eslint/parser',

  plugins: ['@typescript-eslint'],

  rules: {

    // ...

  },

};




module.exports = {

  trailingComma: 'es6',

  tabWidth: 2,

  semi: true,

  singleQuote: true,

};

In this case, both files are valid JavaScript files and do not require any conversion. We can simply remove the type annotations and use the module.exports syntax to export the objects.

These examples demonstrate some common techniques used in idiomatic TypeScript code for custom back-end development at Optimum Web. They include type definitions, async/await, dependency injection, design patterns, and linting/formatting tools.

About the Author: Vladislav Antoseac

Share This Post, Choose Your Platform!

Request a Consultation