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";
In TypeScript, we can specify the expected type:
let age: number = 20;
age = "hello"; // Error
The TypeScript compiler catches this problem before the program runs.
Common Types
String
let name: string = "Koushik";
Number
let age: number = 20;
let price: number = 99.99;
Boolean
let isLoggedIn: boolean = true;
Arrays
let names: string[] = ["Koushik", "Rahul", "John"];
let marks: number[] = [80, 90, 75];
Tuple
A tuple represents an array with a fixed structure:
let user: [string, number] = ["Koushik", 20];
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";
Literal Types
Literal types restrict a value to specific options:
let status: "pending" | "success" | "failed";
Any
any disables most TypeScript type checking:
let data: any = "hello";
data = 100;
data = true;
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());
}
2. Type Inference
TypeScript can automatically determine a variable's type.
let name = "Koushik";
TypeScript understands:
name → string
Therefore, we don't always need to explicitly write the type.
Instead of:
let age: number = 20;
we can often write:
let age = 20;
This is called type inference.
3. Interfaces
An interface defines the expected structure of an object.
interface User {
id: number;
name: string;
email: string;
}
We can then create:
const user: User = {
id: 1,
name: "Koushik",
email: "koushik@example.com"
};
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;
}
Now this is valid:
const user: User = {
id: 1,
name: "Koushik"
};
Readonly Properties
interface User {
readonly id: number;
name: string;
}
Now the name can change, but the ID cannot:
user.name = "Rahul"; // Allowed
user.id = 10; // Error
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;
}
This duplicates the same logic.
With generics:
function identity<T>(value: T): T {
return value;
}
T is a type placeholder.
identity<string>("Koushik");
identity<number>(20);
TypeScript can also infer the type:
identity("Koushik");
identity(20);
5. Generic Functions
Generics are useful for reusable functions.
function getFirst<T>(items: T[]): T {
return items[0];
}
It works with numbers:
const number = getFirst([10, 20, 30]);
And strings:
const name = getFirst(["A", "B", "C"]);
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;
}
For example:
interface User {
id: number;
name: string;
}
const response: ApiResponse<User> = {
data: {
id: 1,
name: "Koushik"
},
success: true
};
Here:
T = User
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;
}
We can test it:
describe("add function", () => {
it("should add two numbers", () => {
expect(add(2, 3)).toBe(5);
});
});
8. describe()
describe() groups related tests.
describe("add function", () => {
});
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", () => {
});
The first argument describes what the test should verify.
10. expect()
expect() verifies the result:
expect(add(2, 3)).toBe(5);
The test checks whether the actual result matches the expected result.
11. Common Jest Matchers
toBe()
expect(2 + 3).toBe(5);
toEqual()
Useful for objects and arrays:
expect({
name: "Koushik"
}).toEqual({
name: "Koushik"
});
toContain()
expect(["A", "B", "C"]).toContain("B");
toThrow()
expect(() => {
throw new Error("Something went wrong");
}).toThrow();
12. Arrange, Act, Assert
A common structure for tests is:
Arrange
↓
Act
↓
Assert
Example:
it("should add two numbers", () => {
// Arrange
const a = 10;
const b = 20;
// Act
const result = add(a, b);
// Assert
expect(result).toBe(30);
});
- Arrange — prepare the data.
- Act — execute the code.
- Assert — verify the result.
13. Jest Mocks
Sometimes code depends on external systems:
Service
↓
Repository
↓
Database
When unit testing the service, we don't necessarily want a real database.
Instead, we can create a mock:
const repository = {
findById: jest.fn()
};
jest.fn() creates a mock function.
We can specify its return value:
repository.findById.mockReturnValue({
id: 1,
name: "Koushik"
});
We can also verify calls:
expect(repository.findById).toHaveBeenCalled();
Or verify arguments:
expect(repository.findById).toHaveBeenCalledWith(1);
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
Each feature can be developed independently.
15. Creating a Feature Branch
First update main:
git switch main
git pull origin main
Create a feature branch:
git switch -c feature/login
Now changes can be made without directly modifying main.
16. Committing Changes
Check changed files:
git status
Stage them:
git add .
Create a commit:
git commit -m "feat: add login functionality"
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
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
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;
}
Generics can create reusable components:
function wrap<T>(data: T) {
return {
data,
success: true
};
}
Jest tests the implementation:
describe("Notification", () => {
it("should create a notification", () => {
// test
});
});
Mocks replace external services:
const emailService = {
send: jest.fn()
};
Git provides the development workflow:
feature/notification-system
↓
Coding
↓
Testing
↓
Commit
↓
Push
↓
PR
↓
Code Review
↓
Feedback
↓
Merge
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
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)