TypeScript Implementation Example
Overviewβ
This document shows a practical example of implementing TypeScript in your existing Attune Logic API with 100% backward compatibility.
Before and After Comparisonβ
Current JavaScript File (controllers/jobs/index.js)β
// Your existing JavaScript file continues to work exactly as before
const Job = require("../../models/Job");
const { general } = require("../../constants");
const getJobs = async (req, res) => {
try {
const { parentCompany } = req.payload;
const { page = 1, limit = 20, status, client } = req.query;
const query = { parentCompany };
if (status) query.status = status;
if (client) query.client = client;
const jobs = await Job.find(query)
.populate("client", "name email")
.limit(limit * 1)
.skip((page - 1) * limit)
.sort({ createdAt: -1 });
const totalCount = await Job.countDocuments(query);
res.json({
status: "success",
data: {
jobs,
totalCount,
totalPages: Math.ceil(totalCount / limit),
currentPage: parseInt(page),
},
});
} catch (error) {
res.status(500).json({
status: "error",
message: error.message,
});
}
};
module.exports = {
getJobs,
// ... other functions
};
New TypeScript File (controllers/jobs/index.ts)β
// Enhanced TypeScript version with full type safety
import { Response } from "express";
import { Job } from "../../models/Job";
import { AuthenticatedRequest, APIResponse, JobDocument } from "../../types";
import { general } from "../../constants";
interface GetJobsQuery {
page?: string;
limit?: string;
status?: string;
client?: string;
}
interface JobsResponse {
jobs: JobDocument[];
totalCount: number;
totalPages: number;
currentPage: number;
}
export const getJobs = async (
req: AuthenticatedRequest<{}, {}, {}, GetJobsQuery>,
res: Response<APIResponse<JobsResponse>>
): Promise<void> => {
try {
const { parentCompany } = req.payload;
const { page = "1", limit = "20", status, client } = req.query;
const query: any = { parentCompany };
if (status) query.status = status;
if (client) query.client = client;
const jobs = await Job.find(query)
.populate("client", "name email")
.limit(Number(limit))
.skip((Number(page) - 1) * Number(limit))
.sort({ createdAt: -1 });
const totalCount = await Job.countDocuments(query);
res.json({
status: "success",
data: {
jobs,
totalCount,
totalPages: Math.ceil(totalCount / Number(limit)),
currentPage: Number(page),
},
});
} catch (error) {
res.status(500).json({
status: "error",
message: error.message,
});
}
};
// Export for backward compatibility
export default {
getJobs,
};
Step-by-Step Implementationβ
Step 1: Add TypeScript Configurationβ
Create tsconfig.json in your project root:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"allowJs": true,
"checkJs": false,
"baseUrl": "./",
"paths": {
"@/*": ["*"],
"@models/*": ["models/*"],
"@controllers/*": ["controllers/*"]
}
},
"include": ["**/*.ts", "**/*.js"],
"exclude": ["node_modules", "dist"]
}
Step 2: Update Package.jsonβ
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"dev:ts": "ts-node-dev --respawn --transpile-only index.ts",
"build": "tsc",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
"@types/express": "^4.17.0",
"@types/mongoose": "^7.4.0",
"ts-node": "^10.9.0",
"ts-node-dev": "^2.0.0"
}
}
Step 3: Create Type Definitionsβ
Create types/index.ts:
import { Request } from "express";
export interface User {
_id: string;
email: string;
firstName: string;
lastName: string;
fullName: string;
authority: "client" | "user" | "admin" | "owner" | "superAdmin";
parentCompany: string;
active: boolean;
}
export interface AuthenticatedRequest<
P = any,
ResBody = any,
ReqBody = any,
ReqQuery = any
> extends Request<P, ResBody, ReqBody, ReqQuery> {
user: User;
payload: User;
parentCompany?: string;
}
export interface APIResponse<T = any> {
status: "success" | "error";
data?: T;
message?: string;
error?: {
code: string;
message: string;
};
}
export interface JobDocument {
_id: string;
client: string;
parentCompany: string;
author: string;
orderNumber: string;
status: string;
createdAt: Date;
updatedAt: Date;
}
Step 4: Test Both JavaScript and TypeScriptβ
Your existing JavaScript files work exactly as before:
# Run your existing JavaScript code
npm run dev
# Run with TypeScript support
npm run dev:ts
# Type check without running
npm run type-check
Gradual Migration Exampleβ
Week 1: Keep Everything JavaScriptβ
// All your existing files remain unchanged
// controllers/jobs/index.js β
(unchanged)
// models/Job.js β
(unchanged)
// routes/api/v1/jobs.js β
(unchanged)
Week 2: Add Type Definitions Onlyβ
// types/index.ts β
(new file)
// tsconfig.json β
(new file)
// All JavaScript files still work β
Week 3: Convert Utilities Firstβ
// utils/response.ts β
(converted)
// utils/validation.ts β
(converted)
// JavaScript files still work β
Week 4: Convert Modelsβ
// models/Job.ts β
(converted)
// models/User.ts β
(converted)
// Old JavaScript imports still work β
Week 5: Convert Controllersβ
// controllers/jobs/index.ts β
(converted)
// controllers/users/index.ts β
(converted)
// Routes can use either JS or TS controllers β
Real-World Migration Exampleβ
Before: controllers/clients/index.jsβ
const Client = require("../../models/Client");
const Job = require("../../models/Job");
const { useCustomerConfig } = require("../../hooks");
const _ = require("lodash");
module.exports = {
getClients: async (req, res) => {
const { parentCompany } = req.payload;
const { page = 1, limit = 20, search = "" } = req.query;
const query = { parentCompany };
if (search) {
query.$or = [
{ name: { $regex: search, $options: "i" } },
{ email: { $regex: search, $options: "i" } },
];
}
const clients = await Client.find(query)
.limit(limit * 1)
.skip((page - 1) * limit)
.sort({ createdAt: -1 });
const totalCount = await Client.countDocuments(query);
res.json({
status: "success",
data: {
clients,
totalCount,
totalPages: Math.ceil(totalCount / limit),
currentPage: parseInt(page),
},
});
},
recentClients: async (req, res) => {
const { parentCompany } = req.payload;
const { appType } = await useCustomerConfig(parentCompany);
let dateField;
if (appType === "trucking") {
dateField = "transactionDate";
} else {
dateField = "appointmentDate";
}
const client = await Job.find({ parentCompany }, "client")
.populate({
path: "client",
select: "slug active email name rates avatar",
})
.sort({ [dateField]: -1 })
.limit(5);
const data = client;
const newData = data
.filter((i) => i.client)
.map((i) => ({ ...i.client.toObject() }));
const uniq = _.uniqBy(newData, (e) => e.name);
return res.status(200).json({
data: uniq,
status: "success",
});
},
};
After: controllers/clients/index.tsβ
import { Response } from "express";
import { Client } from "../../models/Client";
import { Job } from "../../models/Job";
import { useCustomerConfig } from "../../hooks";
import {
AuthenticatedRequest,
APIResponse,
ClientDocument,
Industry,
} from "../../types";
import * as _ from "lodash";
interface GetClientsQuery {
page?: string;
limit?: string;
search?: string;
}
interface ClientsResponse {
clients: ClientDocument[];
totalCount: number;
totalPages: number;
currentPage: number;
}
interface RecentClientsResponse {
data: ClientDocument[];
status: "success";
}
export const getClients = async (
req: AuthenticatedRequest<{}, ClientsResponse, {}, GetClientsQuery>,
res: Response<APIResponse<ClientsResponse>>
): Promise<void> => {
try {
const { parentCompany } = req.payload;
const { page = "1", limit = "20", search = "" } = req.query;
const query: any = { parentCompany };
if (search) {
query.$or = [
{ name: { $regex: search, $options: "i" } },
{ email: { $regex: search, $options: "i" } },
];
}
const clients = await Client.find(query)
.limit(Number(limit))
.skip((Number(page) - 1) * Number(limit))
.sort({ createdAt: -1 });
const totalCount = await Client.countDocuments(query);
res.json({
status: "success",
data: {
clients,
totalCount,
totalPages: Math.ceil(totalCount / Number(limit)),
currentPage: Number(page),
},
});
} catch (error) {
res.status(500).json({
status: "error",
message: error.message,
});
}
};
export const recentClients = async (
req: AuthenticatedRequest,
res: Response<RecentClientsResponse>
): Promise<void> => {
try {
const { parentCompany } = req.payload;
const { appType } = await useCustomerConfig(parentCompany);
let dateField: string;
if (appType === "trucking") {
dateField = "transactionDate";
} else {
dateField = "appointmentDate";
}
const client = await Job.find({ parentCompany }, "client")
.populate({
path: "client",
select: "slug active email name rates avatar",
})
.sort({ [dateField]: -1 })
.limit(5);
const data = client;
const newData = data
.filter((i) => i.client)
.map((i) => ({ ...i.client.toObject() }));
const uniq = _.uniqBy(newData, (e) => e.name);
res.status(200).json({
data: uniq,
status: "success",
});
} catch (error) {
res.status(500).json({
status: "error",
message: error.message,
});
}
};
// Export for backward compatibility
export default {
getClients,
recentClients,
};
Import Compatibilityβ
JavaScript files can import TypeScript files:β
// routes/api/v1/clients.js (unchanged)
const { getClients, recentClients } = require("../../../controllers/clients");
// This works even if controllers/clients is now TypeScript!
TypeScript files can import JavaScript files:β
// controllers/clients/index.ts
import { useCustomerConfig } from "../../hooks"; // .js file
import { Client } from "../../models/Client"; // Could be .js or .ts
Industry-Specific Typesβ
Trucking Industry Typesβ
export interface TruckingJob {
_id: string;
client: string;
parentCompany: string;
loadNumber: string;
transactionDate: Date;
origin: Location;
destination: Location;
driver?: string;
vehicle?: string;
status: "pending" | "assigned" | "in_progress" | "completed";
legs?: TruckingLeg[];
rates?: TruckingRates;
}
export interface TruckingLeg {
_id: string;
job: string;
origin: Location;
destination: Location;
pickupDate: Date;
deliveryDate: Date;
status: "pending" | "picked_up" | "in_transit" | "delivered";
distance?: number;
duration?: number;
}
export interface TruckingRates {
perMile: number;
perHour: number;
fuel: number;
detention: number;
overtime: number;
}
Service & Repair Industry Typesβ
export interface ServiceJob {
_id: string;
client: string;
parentCompany: string;
appointmentDate: Date;
serviceType: "hvac" | "plumbing" | "electrical" | "general";
location: Location;
technician?: string;
status: "scheduled" | "in_progress" | "completed" | "cancelled";
items?: ServiceItem[];
rates?: ServiceRates;
}
export interface ServiceItem {
_id: string;
name: string;
description: string;
quantity: number;
unitPrice: number;
totalPrice: number;
category: string;
}
export interface ServiceRates {
serviceCall: number;
hourly: number;
parts: number;
emergency: number;
holiday: number;
}
Testing Strategyβ
Test Both JavaScript and TypeScriptβ
// tests/controllers/clients.test.js (existing)
const request = require("supertest");
const app = require("../../app");
describe("Clients API", () => {
it("should get clients", async () => {
const response = await request(app).get("/api/v1/clients").expect(200);
expect(response.body.status).toBe("success");
});
});
// tests/controllers/clients.test.ts (new)
import request from "supertest";
import app from "../../app";
import { ClientDocument } from "../../types";
describe("Clients API (TypeScript)", () => {
it("should get clients with proper typing", async () => {
const response = await request(app).get("/api/v1/clients").expect(200);
expect(response.body.status).toBe("success");
expect(Array.isArray(response.body.data.clients)).toBe(true);
// Type-safe access to client properties
const clients: ClientDocument[] = response.body.data.clients;
if (clients.length > 0) {
expect(clients[0]).toHaveProperty("_id");
expect(clients[0]).toHaveProperty("name");
expect(clients[0]).toHaveProperty("email");
}
});
});
Benefits You'll See Immediatelyβ
1. Better IDE Supportβ
// Auto-completion and IntelliSense
req.payload.res // Shows: id, email, firstName, lastName, etc.
.json({
status: "success", // Auto-suggests 'success' | 'error'
data: clients, // Shows available properties
});
2. Catch Errors Earlyβ
// TypeScript catches this error at compile time
const client = await Client.findById(req.params.id);
res.json({
status: "success",
data: client.nonExistentProperty, // β Error: Property doesn't exist
});
3. Better Refactoringβ
// Rename a property and TypeScript updates all references
interface User {
fullName: string; // Rename from 'name'
}
// All uses of 'name' will show as errors until updated
4. Industry-Specific Type Safetyβ
// Trucking-specific validation
const createTruckingJob = (data: TruckingJob) => {
// TypeScript ensures all required fields are present
if (!data.loadNumber) {
// β
Type-safe
throw new Error("Load number is required");
}
};
// Service-specific validation
const createServiceJob = (data: ServiceJob) => {
// TypeScript ensures proper service types
if (!["hvac", "plumbing", "electrical"].includes(data.serviceType)) {
throw new Error("Invalid service type");
}
};
Migration Timelineβ
Week 1: Setupβ
- Install TypeScript dependencies β
- Configure tsconfig.json β
- Test existing code still works β
Week 2: Typesβ
- Create type definitions β
- Define industry-specific types β
- Test type checking β
Week 3-4: Utilities & Modelsβ
- Convert utility functions β
- Migrate model files β
- Update imports gradually β
Week 5-6: Controllersβ
- Convert controllers one by one β
- Update route handlers β
- Test mixed JS/TS codebase β
Result: Best of Both Worldsβ
// Your existing JavaScript files continue to work
// controllers/jobs/legacy.js β
// models/OldModel.js β
// routes/api/v1/legacy.js β
// New TypeScript files provide enhanced development experience
// controllers/jobs/index.ts β
(type-safe)
// models/Job.ts β
(type-safe)
// types/index.ts β
(industry-specific types)
Zero risk, maximum benefit! Your production code remains stable while new development gets all the benefits of TypeScript.
Ready to get started? Begin with Phase 1 of the migration guide!