What is TypeScript and why learn it in 2026?

TypeScript is a superset of JavaScript that adds static type definitions, enabling IDEs and compilers to catch mistakes before they reach production. In 2026, the ecosystem has matured: the compiler is faster, the @types ecosystem covers virtually every npm package, and frameworks like NestJS and Fastify ship with first‑class TypeScript support. Learning TypeScript now means you’ll write fewer runtime bugs, enjoy better autocompletion, and make your code self‑documenting—critical when working on distributed teams or open‑source projects.
Setting up your Node.js API project

First, create a fresh folder and initialize a Node.js project:
mkdir typed-api && cd typed-api
npm init -y
Install the core dependencies:
npm i express typescript @types/node @types/express
npm i -D ts-node-dev nodemon
Add a basic tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
Create a src folder and an entry point src/server.ts:
import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'ok' });
});
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => console.log(`🚀 Server running on http://localhost:${PORT}`));
Add a dev script to package.json:
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
Run npm dev and visit http://localhost:3000/health—you should see { "status": "ok" }. Your API is now TypeScript‑powered.
Defining strong types with interfaces and generics
Let’s model a simple resource: a Task for a to‑do list. In src/types.ts:
export interface Task {
id: string;
title: string;
description?: string;
completed: boolean;
createdAt: Date;
}
We’ll store tasks in memory for now, but the interface gives us compile‑time safety everywhere we use a Task. Next, create a generic repository pattern that works with any entity:
// src/repository.ts
export interface Repository<T> {
findAll(): Promise<T[]>;
findById(id: string): Promise<T | null>;
create(item: Omit<T, 'id'>): Promise<T>;
update(id: string, changes: Partial<T>): Promise<T | null>;
delete(id: string): Promise<boolean>;
}
export class InMemoryRepository<T> implements Repository<T> {
private store: Map<string, T> = new Map();
async findAll(): Promise<T[]> {
return Array.from(this.store.values());
}
async findById(id: string): Promise<T | null> {
return this.store.get(id) ?? null;
}
async create(item: Omit<T, 'id'>): Promise<T> {
const id = crypto.randomUUID();
const newItem = { ...item, id } as T;
this.store.set(id, newItem);
return newItem;
}
async update(id: string, changes: Partial<T>): Promise<T | null> {
const existing = await this.findById(id);
if (!existing) return null;
const updated = { ...existing, ...changes };
this.store.set(id, updated);
return updated;
}
async delete(id: string): Promise<boolean> {
return this.store.delete(id);
}
}
Now we can instantiate a typed repository for Task:
// src/taskRepository.ts
import { InMemoryRepository } from './repository';
import { Task } from './types';
export const taskRepo = new InMemoryRepository<Task>();
Because the repository is generic, switching to a PostgreSQL or MongoDB implementation later only requires changing the class—your service layer stays untouched.
Adding validation and error handling
Even with types, incoming data needs runtime validation. We’ll use zod, a schema‑first validation library that infers TypeScript types.
npm i zod
Create a validation schema for creating a task:
// src/validation.ts
import { z } from 'zod';
export const createTaskSchema = z.object({
title: z.string().min(1, 'Title is required'),
description: z.string().optional(),
completed: z.boolean().default(false),
});
export type CreateTaskInput = z.infer<typeof createTaskSchema>;
In the route handler, we validate before calling the repository:
// src/routes/taskRoutes.ts
import { Router, Request, Response, NextFunction } from 'express';
import { taskRepo } from '../taskRepository';
import { createTaskSchema, CreateTaskInput } from '../validation';
const router = Router();
router.get('/', async (_req: Request, res: Response) => {
const tasks = await taskRepo.findAll();
res.json(tasks);
});
router.post(
'/',
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsed = createTaskSchema.parse(req.body); // throws if invalid
const newTask = await taskRepo.create(parsed as CreateTaskInput);
res.status(201).json(newTask);
} catch (err) {
next(err); // pass to error‑handling middleware
}
}
);
// Generic error handler
router.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
if (err instanceof z.ZodError) {
return res.status(400).json({ errors: err.errors });
}
console.error(err);
res.status(500).json({ message: 'Internal server error' });
});
export default router;
Register the router in server.ts:
import taskRoutes from './routes/taskRoutes';
// ...
app.use('/tasks', taskRoutes);
Now invalid payloads return a clear 400 response with field‑level details, while unexpected errors are logged and turned into a generic 500.
Connecting to a database with typed models
For a real API, swap the in‑memory store for a PostgreSQL client with type‑safe query building. We’ll use pg and slonik (which offers TypeScript‑first SQL templates).
npm i pg slonik
Add a database connection wrapper:
// src/db.ts
import { createPool, Pool } from 'slonik';
export const pool: Pool = createPool(process.env.DATABASE_URL ?? '', {
// optional: max connections, etc.
});
Define a SQL fragment for inserting a task and infer its return type:
// src/taskSql.ts
import { sql } from 'slonik';
import { Task } from '../types';
export const insertTask = sql`
INSERT INTO tasks (id, title, description, completed, created_at)
VALUES (
${sql.parameter('id')},
${sql.parameter('title')},
${sql.parameter('description')},
${sql.parameter('completed')},
${sql.parameter('createdAt')}
)
RETURNING *
` as unknown as (params: {
id: string;
title: string;
description?: string | null;
completed: boolean;
createdAt: Date;
}) => Promise<Task>;
Now the repository method becomes:
async create(item: Omit<Task, 'id'>): Promise<Task> {
const id = crypto.randomUUID();
const now = new Date();
const result = await pool.one(insertTask, {
id,
title: item.title,
description: item.description ?? null,
completed: item.completed,
createdAt: now,
});
return result;
}
Because Slonik’s template literals are typed, any mismatch between the SQL columns and the TypeScript interface triggers a compile error—preventing drift between schema and code.
Testing and deploying your typed API
Write a quick test with Jest and supertest to assert the API contract:
npm i -D jest ts-jest @types/jest supertest @types/supertest
jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
src/taskRoutes.test.ts:
import request from 'supertest';
import express from 'express';
import taskRoutes from './routes/taskRoutes';
const app = express();
app.use(express.json());
app.use('/tasks', taskRoutes);
describe('Task API', () => {
it('creates a task and returns 201', async () => {
const res = await request(app)
.post('/tasks')
.send({ title: 'Learn TypeScript' })
.expect(201);
expect(res.body).toHaveProperty('id');
expect(res.body.title).toBe('Learn TypeScript');
});
it('rejects missing title with 400', async () => {
await request(app)
.post('/tasks')
.send({})
.expect(400)
.expect(res => {
expect(res.body.errors).toEqual(
expect.arrayContaining([
expect.objectContaining({ path: ['title'] }),
])
);
});
});
});
Run npm test (add "test": "jest" to scripts) and watch the green checkmarks.
For deployment, compile to JavaScript and run the built output:
npm run build
npm start
If you prefer containers, a minimal Dockerfile looks like:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "dist/server.js"]
Push the image to any registry and deploy to your favorite platform (AWS ECS, Fly.io, Render, etc.). Because the API is typed, you’ll catch integration bugs early in CI pipelines—saving you from midnight production fires.
Let's Work Together
Ready to build safer, more maintainable APIs with TypeScript? I’m Rasel Hossain, a Full‑Stack Developer and AI Automation Engineer with 6+ years of experience and 168+ successful Fiverr projects. Let’s discuss how we can bring strong types to your next Node.js or NestJS backend.
Email
WhatsApp
Phone: +8801757220402
Frequently Asked Questions
Q: Do I need to install a separate type definition package for every library I use?
A: Most popular npm packages ship with built‑in types or have community‑maintained @types equivalents. If a library lacks types, you can write a simple declaration file or use the any type as a temporary fallback while you contribute types back to the community.
Q: How does TypeScript affect bundle size for a Node.js API?
A: TypeScript is a development‑time tool; the compiled JavaScript output is essentially the same size as hand‑written JS. The only overhead is the .ts source files in your repo, which are ignored in production builds.
Q: Can I use TypeScript with serverless platforms like AWS Lambda?
A: Absolutely. Compile your TypeScript to JavaScript, upload the dist folder, and configure the Lambda handler to point to the compiled entry point. Many frameworks (e.g., Serverless Framework) have built‑in TypeScript support.
Q: Is it worth migrating an existing JavaScript API to TypeScript?
A: If the codebase is actively maintained, incremental migration pays off quickly. Start by adding // @ts-check to critical files, then convert modules one by one, leveraging the compiler’s strict mode to uncover hidden bugs.
Q: What’s the difference between interface and type in TypeScript?
A: Both describe shapes, but interface is preferred for object shapes that may be extended or implemented by classes, while type excels at unions, tuples, and mapped types. Choose based on readability and the features you need.
HowTo: Build Your First Typed API in 10 Minutes
- Initialize a new Node.js project (
npm init -y) and installexpress,typescript, and@typespackages. - Create a
tsconfig.jsonwithstrict: trueand set up asrc/server.tsfile that boots a basic Express app. - Define your data shape using an
interface(e.g.,Task) and build a generic repository to handle CRUD operations. - Add runtime validation with
zodfor incoming requests and attach a central error‑handling middleware. - Compile (
npm run build) and start the server (npm start); test endpoints with a tool like Postman orcurlto see typed responses in action.
By following these steps, you’ll have a production‑ready API that leverages TypeScript’s compile‑time safety, giving you fewer bugs and faster iteration cycles.
Take the first step toward safer code today—reach out, and let’s build something great together.