Skip to main content

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!