TypeScript Setup
Add TypeScript to your Node.js project - configuration, compilation, and development workflow.
6 min read
Why TypeScript?#
JavaScript lets you pass anything anywhere. TypeScript catches mistakes before they crash production:
typescript
// JavaScript: Crashes at runtime
function getUser(id) {
return users.find(u => u.id === id);
}
getUser("123"); // Oops, id should be a number
// TypeScript: Catches at compile time
function getUser(id: number): User | undefined {
return users.find(u => u.id === id);
}
getUser("123"); // Error: Argument of type 'string' is not assignable
Quick Start#
Initialize TypeScript#
bash
npm install -D typescript @types/node
# Generate tsconfig.json
npx tsc --init
Recommended tsconfig.json#
json
{
"compilerOptions": {
// Output
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
// Strict mode (use all of these)
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
// Module interop
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
// Other
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Project Structure#
my-api/
├── src/
│ ├── index.ts # Entry point
│ ├── config/
│ ├── routes/
│ ├── controllers/
│ ├── services/
│ ├── models/
│ ├── middleware/
│ └── types/ # Custom type definitions
│ └── index.ts
├── dist/ # Compiled JavaScript (git-ignored)
├── tsconfig.json
└── package.json
Development Workflow#
Option 1: ts-node (Simple)#
Run TypeScript directly without compiling:
bash
npm install -D ts-node
json
{
"scripts": {
"dev": "ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
Option 2: tsx (Faster)#
Modern, faster alternative to ts-node:
bash
npm install -D tsx
json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
Option 3: nodemon + ts-node (Auto-restart)#
bash
npm install -D nodemon ts-node
json
{
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts"
}
}
json
// nodemon.json
{
"watch": ["src"],
"ext": "ts,json",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node ./src/index.ts"
}
Option 4: SWC (Fastest Compilation)#
bash
npm install -D @swc/core @swc/cli
json
// .swcrc
{
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true
},
"target": "es2022"
},
"module": {
"type": "es6"
}
}
json
{
"scripts": {
"build": "swc src -d dist",
"dev": "swc src -d dist --watch"
}
}
Type Definitions#
Install Type Packages#
bash
# Express types
npm install -D @types/express @types/cors @types/morgan
# Other common packages
npm install -D @types/bcryptjs @types/jsonwebtoken @types/uuid
Check for Types#
bash
# Search for types on DefinitelyTyped
npm search @types/package-name
Type Coverage
Most popular packages have types. If a package doesn't have types, you can:
- Check if it's built with TypeScript (types included)
- Create a declaration file (.d.ts)
- Use
// @ts-ignoreas last resort
Express with TypeScript#
Basic Setup#
typescript
// src/index.ts
import express, { Application } from 'express';
import cors from 'cors';
import { config } from './config/index.js';
import { errorHandler } from './middleware/errorHandler.js';
import userRoutes from './routes/users.js';
const app: Application = express();
app.use(cors());
app.use(express.json());
app.use('/api/users', userRoutes);
app.use(errorHandler);
app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`);
});
Typed Request Handlers#
typescript
// src/controllers/userController.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/userService.js';
export async function getUser(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const user = await UserService.findById(req.params.id);
res.json({ data: user });
} catch (error) {
next(error);
}
}
Custom Request Types#
typescript
// src/types/index.ts
import { Request } from 'express';
export interface AuthenticatedRequest extends Request {
user: {
id: string;
email: string;
role: 'user' | 'admin';
};
}
// Usage in controller
export async function getProfile(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
const userId = req.user.id; // Typed!
// ...
}
Typed Route Parameters#
typescript
// src/routes/users.ts
import { Router, Request, Response } from 'express';
interface UserParams {
id: string;
}
interface CreateUserBody {
email: string;
password: string;
name: string;
}
const router = Router();
router.get('/:id', async (req: Request<UserParams>, res: Response) => {
const { id } = req.params; // id is string
// ...
});
router.post('/', async (
req: Request<{}, {}, CreateUserBody>,
res: Response
) => {
const { email, password, name } = req.body; // All typed!
// ...
});
export default router;
Environment Variables#
Type-safe Config#
typescript
// src/config/index.ts
import { z } from 'zod';
import dotenv from 'dotenv';
dotenv.config();
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().transform(Number).default('3000'),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string().default('7d'),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('Invalid environment variables:', parsed.error.format());
process.exit(1);
}
export const config = parsed.data;
export type Config = z.infer<typeof envSchema>;
Path Aliases#
Configure Paths#
json
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/config": ["src/config/index.ts"],
"@/types": ["src/types/index.ts"]
}
}
}
Make Node Understand Paths#
bash
npm install -D tsconfig-paths
json
{
"scripts": {
"dev": "ts-node -r tsconfig-paths/register src/index.ts"
}
}
Or with tsx (no additional setup needed):
json
{
"scripts": {
"dev": "tsx watch src/index.ts"
}
}
Build for Production#
Standard Build#
json
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
With Path Alias Resolution#
bash
npm install -D tsc-alias
json
{
"scripts": {
"build": "tsc && tsc-alias"
}
}
Docker with TypeScript#
dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
Comparison: Development Tools#
| Tool | Speed | Features | Best For |
|---|---|---|---|
| ts-node | Slow | Full TS support | Quick scripts |
| tsx | Fast | ESM, watch mode | Development |
| nodemon + ts-node | Slow | Auto-restart | Legacy projects |
| SWC | Fastest | Build only | Production builds |
Recommendation
Use tsx for development (fast, zero-config) and tsc for production builds (full type checking).
Key Takeaways#
- Start with strict mode - Catch more errors early
- Use tsx for development - Fast, ESM support, watch mode
- Type your environment - Zod + dotenv for type-safe config
- Extend Request type - For authenticated routes
- Build with tsc - Full type checking for production
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.