DEV Community

koushikmaya
koushikmaya

Posted on

# Week 6 Task 2: TypeScript, Jest, Git Workflow & Code Review

Introduction

As JavaScript applications become larger, maintaining type safety, writing reliable tests, and collaborating with other developers becomes increasingly important.

In this task, I learned several practical concepts commonly used in real-world TypeScript projects:

  • TypeScript basic types
  • Interfaces
  • Generics
  • Jest fundamentals
  • Mocks
  • Git feature branches
  • Pull Requests
  • Code review etiquette

These concepts work together as part of a professional development workflow.


1. TypeScript Basic Types

TypeScript is a superset of JavaScript that adds static typing.

JavaScript allows a variable to change its type:

let age = 20;
age = "hello";
Enter fullscreen mode Exit fullscreen mode

In TypeScript, we can specify the expected type:

let age: number = 20;

age = "hello"; // Error
Enter fullscreen mode Exit fullscreen mode

The TypeScript compiler catches this problem before the program runs.

Common Types

String

let name: string = "Koushik";
Enter fullscreen mode Exit fullscreen mode

Number

let age: number = 20;
let price: number = 99.99;
Enter fullscreen mode Exit fullscreen mode

Boolean

let isLoggedIn: boolean = true;
Enter fullscreen mode Exit fullscreen mode

Arrays

let names: string[] = ["Koushik", "Rahul", "John"];
let marks: number[] = [80, 90, 75];
Enter fullscreen mode Exit fullscreen mode

Tuple

A tuple represents an array with a fixed structure:

let user: [string, number] = ["Koushik", 20];
Enter fullscreen mode Exit fullscreen mode

The first value must be a string and the second must be a number.

Union Types

A union allows multiple possible types:

let id: string | number;

id = 10;
id = "abc123";
Enter fullscreen mode Exit fullscreen mode

Literal Types

Literal types restrict a value to specific options:

let status: "pending" | "success" | "failed";
Enter fullscreen mode Exit fullscreen mode

Any

any disables most TypeScript type checking:

let data: any = "hello";

data = 100;
data = true;
Enter fullscreen mode Exit fullscreen mode

Using any everywhere defeats much of the purpose of TypeScript.

Unknown

unknown is safer than any:

let data: unknown = "hello";

if (typeof data === "string") {
  console.log(data.toUpperCase());
}
Enter fullscreen mode Exit fullscreen mode

2. Type Inference

TypeScript can automatically determine a variable's type.

let name = "Koushik";
Enter fullscreen mode Exit fullscreen mode

TypeScript understands:

name → string
Enter fullscreen mode Exit fullscreen mode

Therefore, we don't always need to explicitly write the type.

Instead of:

let age: number = 20;
Enter fullscreen mode Exit fullscreen mode

we can often write:

let age = 20;
Enter fullscreen mode Exit fullscreen mode

This is called type inference.


3. Interfaces

An interface defines the expected structure of an object.

interface User {
  id: number;
  name: string;
  email: string;
}
Enter fullscreen mode Exit fullscreen mode

We can then create:

const user: User = {
  id: 1,
  name: "Koushik",
  email: "koushik@example.com"
};
Enter fullscreen mode Exit fullscreen mode

The interface acts like a contract. Objects using the interface must follow its structure.

Optional Properties

Use ? for optional properties:

interface User {
  id: number;
  name: string;
  email?: string;
}
Enter fullscreen mode Exit fullscreen mode

Now this is valid:

const user: User = {
  id: 1,
  name: "Koushik"
};
Enter fullscreen mode Exit fullscreen mode

Readonly Properties

interface User {
  readonly id: number;
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

Now the name can change, but the ID cannot:

user.name = "Rahul"; // Allowed
user.id = 10;        // Error
Enter fullscreen mode Exit fullscreen mode

4. Generics

Generics allow us to write reusable and type-safe code.

Without generics:

function identity(value: string): string {
  return value;
}

function identityNumber(value: number): number {
  return value;
}
Enter fullscreen mode Exit fullscreen mode

This duplicates the same logic.

With generics:

function identity<T>(value: T): T {
  return value;
}
Enter fullscreen mode Exit fullscreen mode

T is a type placeholder.

identity<string>("Koushik");
identity<number>(20);
Enter fullscreen mode Exit fullscreen mode

TypeScript can also infer the type:

identity("Koushik");
identity(20);
Enter fullscreen mode Exit fullscreen mode

5. Generic Functions

Generics are useful for reusable functions.

function getFirst<T>(items: T[]): T {
  return items[0];
}
Enter fullscreen mode Exit fullscreen mode

It works with numbers:

const number = getFirst([10, 20, 30]);
Enter fullscreen mode Exit fullscreen mode

And strings:

const name = getFirst(["A", "B", "C"]);
Enter fullscreen mode Exit fullscreen mode

The same function works with different types while maintaining type safety.


6. Generic Interfaces

Generics can also be used with interfaces:

interface ApiResponse<T> {
  data: T;
  success: boolean;
}
Enter fullscreen mode Exit fullscreen mode

For example:

interface User {
  id: number;
  name: string;
}

const response: ApiResponse<User> = {
  data: {
    id: 1,
    name: "Koushik"
  },
  success: true
};
Enter fullscreen mode Exit fullscreen mode

Here:

T = User
Enter fullscreen mode Exit fullscreen mode

The same ApiResponse<T> can be reused with other types.


7. Jest Fundamentals

Jest is a JavaScript and TypeScript testing framework.

Suppose we have:

function add(a: number, b: number): number {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

We can test it:

describe("add function", () => {
  it("should add two numbers", () => {
    expect(add(2, 3)).toBe(5);
  });
});
Enter fullscreen mode Exit fullscreen mode

8. describe()

describe() groups related tests.

describe("add function", () => {

});
Enter fullscreen mode Exit fullscreen mode

It means that the tests inside the block are related to the add function.


9. it()

it() defines an individual test:

it("should add two numbers", () => {

});
Enter fullscreen mode Exit fullscreen mode

The first argument describes what the test should verify.


10. expect()

expect() verifies the result:

expect(add(2, 3)).toBe(5);
Enter fullscreen mode Exit fullscreen mode

The test checks whether the actual result matches the expected result.


11. Common Jest Matchers

toBe()

expect(2 + 3).toBe(5);
Enter fullscreen mode Exit fullscreen mode

toEqual()

Useful for objects and arrays:

expect({
  name: "Koushik"
}).toEqual({
  name: "Koushik"
});
Enter fullscreen mode Exit fullscreen mode

toContain()

expect(["A", "B", "C"]).toContain("B");
Enter fullscreen mode Exit fullscreen mode

toThrow()

expect(() => {
  throw new Error("Something went wrong");
}).toThrow();
Enter fullscreen mode Exit fullscreen mode

12. Arrange, Act, Assert

A common structure for tests is:

Arrange
   ↓
Act
   ↓
Assert
Enter fullscreen mode Exit fullscreen mode

Example:

it("should add two numbers", () => {
  // Arrange
  const a = 10;
  const b = 20;

  // Act
  const result = add(a, b);

  // Assert
  expect(result).toBe(30);
});
Enter fullscreen mode Exit fullscreen mode
  • Arrange — prepare the data.
  • Act — execute the code.
  • Assert — verify the result.

13. Jest Mocks

Sometimes code depends on external systems:

Service
   ↓
Repository
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

When unit testing the service, we don't necessarily want a real database.

Instead, we can create a mock:

const repository = {
  findById: jest.fn()
};
Enter fullscreen mode Exit fullscreen mode

jest.fn() creates a mock function.

We can specify its return value:

repository.findById.mockReturnValue({
  id: 1,
  name: "Koushik"
});
Enter fullscreen mode Exit fullscreen mode

We can also verify calls:

expect(repository.findById).toHaveBeenCalled();
Enter fullscreen mode Exit fullscreen mode

Or verify arguments:

expect(repository.findById).toHaveBeenCalledWith(1);
Enter fullscreen mode Exit fullscreen mode

Mocks make unit tests faster, more predictable, and independent of external systems.


14. Git Feature Branches

Git helps developers collaborate safely.

Instead of making every change directly on main, developers create feature branches:

main
 |
 ├── feature/login
 ├── feature/signup
 └── feature/profile
Enter fullscreen mode Exit fullscreen mode

Each feature can be developed independently.


15. Creating a Feature Branch

First update main:

git switch main
git pull origin main
Enter fullscreen mode Exit fullscreen mode

Create a feature branch:

git switch -c feature/login
Enter fullscreen mode Exit fullscreen mode

Now changes can be made without directly modifying main.


16. Committing Changes

Check changed files:

git status
Enter fullscreen mode Exit fullscreen mode

Stage them:

git add .
Enter fullscreen mode Exit fullscreen mode

Create a commit:

git commit -m "feat: add login functionality"
Enter fullscreen mode Exit fullscreen mode

A commit is a logical checkpoint in the project's history.


17. Pushing the Branch

Push the branch to GitHub:

git push -u origin feature/login
Enter fullscreen mode Exit fullscreen mode

Now the branch exists locally and on GitHub.


18. Pull Requests

After completing the feature, create a Pull Request.

A PR means:

"I have completed this change. Please review it before it becomes part of the main codebase."

Typical workflow:

Create branch
      ↓
Write code
      ↓
Run tests
      ↓
Commit
      ↓
Push
      ↓
Create PR
      ↓
Code review
      ↓
Fix feedback
      ↓
Approval
      ↓
Merge
Enter fullscreen mode Exit fullscreen mode

19. Why Pull Requests Matter

PRs provide a checkpoint before code reaches the main branch.

They allow a team to:

  • Review implementation
  • Find bugs
  • Discuss design decisions
  • Check tests
  • Improve code quality
  • Maintain project standards

20. Code Review Etiquette

Code review should be treated as collaboration.

The purpose is not to criticize the developer. The purpose is to improve the code.

Review the Code, Not the Person

Instead of:

"You don't know TypeScript."

A better comment is:

"Could we define an interface here so the expected object structure is explicit?"

Focus on the code, not the person.

Explain Why

Instead of:

"Change this."

Explain the reason:

"Could we extract this logic into a helper? It is repeated in multiple places, so extraction would make future changes easier."

Ask Questions

Instead of:

"Use an interface."

A more collaborative comment is:

"Would an interface make the expected structure clearer here?"

Avoid Unnecessary Nitpicking

Not every difference needs to be a blocking comment.

Reviews should focus on:

  • Correctness
  • Security
  • Architecture
  • Maintainability
  • Performance
  • Tests
  • Type safety
  • Project conventions

21. Blocking Issues vs Suggestions

It is useful to distinguish between required fixes and optional improvements.

Blocking comment

"This allows duplicate records. Can we enforce uniqueness at the database level?"

Suggestion

"Optional: this helper could be extracted to make the function easier to read."

This helps the author understand which changes are required before merging.


22. Review Tests Too

Code review should cover tests as well as production code.

A reviewer should ask:

  • Does the test actually verify the behavior?
  • Are important edge cases covered?
  • Are mocks being used correctly?
  • Is the test unnecessarily dependent on external systems?
  • Are failure cases tested?

Having many tests doesn't automatically mean the code is well tested. The tests need to verify meaningful behavior.


23. Putting Everything Together

These concepts form one practical development workflow.

Imagine building a notification system.

TypeScript defines the structure:

interface Notification {
  recipient: string;
  message: string;
}
Enter fullscreen mode Exit fullscreen mode

Generics can create reusable components:

function wrap<T>(data: T) {
  return {
    data,
    success: true
  };
}
Enter fullscreen mode Exit fullscreen mode

Jest tests the implementation:

describe("Notification", () => {
  it("should create a notification", () => {
    // test
  });
});
Enter fullscreen mode Exit fullscreen mode

Mocks replace external services:

const emailService = {
  send: jest.fn()
};
Enter fullscreen mode Exit fullscreen mode

Git provides the development workflow:

feature/notification-system
          ↓
        Coding
          ↓
        Testing
          ↓
        Commit
          ↓
         Push
          ↓
          PR
          ↓
     Code Review
          ↓
       Feedback
          ↓
        Merge
Enter fullscreen mode Exit fullscreen mode

24. What I Learned

Professional development isn't only about writing code.

It involves several connected practices:

TypeScript
   ↓
Type-safe code

Interfaces
   ↓
Clear contracts

Generics
   ↓
Reusable type-safe code

Jest
   ↓
Automated testing

Mocks
   ↓
Isolated unit tests

Git branches
   ↓
Safe development

Pull Requests
   ↓
Team collaboration

Code Review
   ↓
Better code quality
Enter fullscreen mode Exit fullscreen mode

Understanding these concepts provides a strong foundation for larger TypeScript and Node.js applications.


Conclusion

TypeScript provides type safety and helps catch many errors before runtime. Interfaces allow developers to define clear contracts, while generics make code reusable without sacrificing type safety.

Jest provides automated testing, and mocks allow individual components to be tested without depending on external systems.

Git feature branches and Pull Requests provide a structured way for developers to collaborate, while code reviews help catch problems and improve maintainability.

Together, these practices form an important part of a professional software development workflow.

The main lesson is that writing code is only one part of software engineering. Writing maintainable code, testing it, collaborating through Git, and reviewing each other's work are equally important.

Top comments (0)