Skip to main content

TypeScript Migration Guide for Attune Logic API

Overview​

This guide provides a safe, gradual migration path to add TypeScript to your existing Express.js API while maintaining 100% backward compatibility with your current JavaScript codebase.

Migration Strategy: Gradual Adoption​

Phase 1: Setup TypeScript Infrastructure (Week 1)​

  • Add TypeScript tooling without changing existing code
  • Configure TypeScript to work alongside JavaScript
  • Set up type definitions for existing dependencies
  • Ensure existing build process continues to work

Phase 2: Type Definitions (Week 2)​

  • Create type definitions for existing models and APIs
  • Add types for request/response objects
  • Define industry-specific types (trucking, service/repair)
  • Maintain full JavaScript compatibility

Phase 3: Gradual File Migration (Week 3-6)​

  • Convert utilities and helpers first
  • Migrate models and schemas
  • Convert controllers one by one
  • Keep existing JavaScript files working

Phase 4: Enhanced TypeScript Features (Week 7-8)​

  • Add strict type checking
  • Implement advanced TypeScript features
  • Optimize build process
  • Full type safety across the codebase

Phase 1: TypeScript Infrastructure Setup​

1. Install TypeScript Dependencies​

# Install TypeScript and related tools
npm install --save-dev typescript @types/node @types/express @types/mongoose
npm install --save-dev @types/cors @types/cookie-parser @types/bcrypt
npm install --save-dev @types/jsonwebtoken @types/multer @types/nodemailer
npm install --save-dev @types/passport @types/passport-local
npm install --save-dev ts-node ts-node-dev nodemon
npm install --save-dev @types/jest

# Optional: Install utility types
npm install --save-dev utility-types

2. TypeScript Configuration (tsconfig.json)​

{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noImplicitAny": false,
"strictNullChecks": false,
"strictFunctionTypes": false,
"noImplicitReturns": false,
"noImplicitThis": false,
"allowJs": true,
"checkJs": false,
"baseUrl": "./",
"paths": {
"@/*": ["*"],
"@models/*": ["models/*"],
"@controllers/*": ["controllers/*"],
"@middlewares/*": ["middlewares/*"],
"@services/*": ["services/*"],
"@utils/*": ["utils/*"],
"@config/*": ["config/*"],
"@constants/*": ["constants/*"],
"@hooks/*": ["hooks/*"]
}
},
"include": ["**/*.ts", "**/*.js"],
"exclude": [
"node_modules",
"dist",
"build",
"uploads",
"tests/**/*.test.js",
"tests/**/*.test.ts"
]
}

3. Update Package.json Scripts​

{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"dev:ts": "ts-node-dev --respawn --transpile-only index.ts",
"build": "tsc",
"build:watch": "tsc --watch",
"type-check": "tsc --noEmit",
"test": "NODE_ENV=test jest --detectOpenHandles --forceExit --testTimeout=30000",
"test:ts": "NODE_ENV=test jest --preset ts-jest --detectOpenHandles --forceExit --testTimeout=30000"
}
}

4. Update Nodemon Configuration (nodemon.json)​

{
"watch": ["**/*.js", "**/*.ts"],
"ext": "js,ts,json",
"ignore": ["node_modules", "dist", "build"],
"exec": "node index.js",
"env": {
"NODE_ENV": "development"
}
}

5. Jest Configuration for TypeScript (jest.config.js)​

module.exports = {
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>/tests"],
testMatch: ["**/__tests__/**/*.(js|ts)", "**/*.(test|spec).(js|ts)"],
transform: {
"^.+\\.ts$": "ts-jest",
"^.+\\.js$": "babel-jest",
},
moduleFileExtensions: ["ts", "js", "json"],
collectCoverageFrom: [
"controllers/**/*.(js|ts)",
"models/**/*.(js|ts)",
"services/**/*.(js|ts)",
"utils/**/*.(js|ts)",
"!**/*.d.ts",
],
moduleNameMapping: {
"^@/(.*)$": "<rootDir>/$1",
"^@models/(.*)$": "<rootDir>/models/$1",
"^@controllers/(.*)$": "<rootDir>/controllers/$1",
"^@middlewares/(.*)$": "<rootDir>/middlewares/$1",
"^@services/(.*)$": "<rootDir>/services/$1",
"^@utils/(.*)$": "<rootDir>/utils/$1",
"^@config/(.*)$": "<rootDir>/config/$1",
"^@constants/(.*)$": "<rootDir>/constants/$1",
"^@hooks/(.*)$": "<rootDir>/hooks/$1",
},
};

Phase 2: Type Definitions​

1. Core Type Definitions (types/index.ts)​

// Core API types
export interface APIResponse<T = any> {
status: "success" | "error";
data?: T;
message?: string;
error?: {
code: string;
message: string;
details?: any;
};
}

// User and Authentication types
export interface User {
_id: string;
email: string;
firstName: string;
lastName: string;
fullName: string;
authority: UserRole;
active: boolean;
parentCompany: string;
type?: string;
company?: string;
meta?: {
role?: string;
permissions?: string[];
};
createdAt: Date;
updatedAt: Date;
}

export type UserRole = "client" | "user" | "admin" | "owner" | "superAdmin";

// Multi-tenant types
export interface ParentCompany {
_id: string;
name: string;
industry: Industry;
slug: string;
active: boolean;
settings?: CompanySettings;
createdAt: Date;
updatedAt: Date;
}

export type Industry = "trucking" | "service-repair";

export interface CompanySettings {
features?: FeatureSettings;
rates?: RateSettings;
branding?: BrandingSettings;
}

// Request/Response types
export interface AuthenticatedRequest extends Request {
user: User;
payload: User;
parentCompany?: string;
customerConfig?: CustomerConfig;
}

export interface PaginatedResponse<T> {
data: T[];
totalCount: number;
totalPages: number;
currentPage: number;
hasNext: boolean;
hasPrev: boolean;
}

// Industry-specific types
export interface TruckingJob {
_id: string;
client: string;
parentCompany: string;
author: string;
orderNumber: string;
loadNumber: string;
transactionDate: Date;
origin: Location;
destination: Location;
driver?: string;
vehicle?: string;
status: TruckingJobStatus;
legs?: TruckingLeg[];
rates?: TruckingRates;
createdAt: Date;
updatedAt: Date;
}

export type TruckingJobStatus =
| "pending"
| "assigned"
| "in_progress"
| "completed"
| "cancelled";

export interface TruckingLeg {
_id: string;
job: string;
origin: Location;
destination: Location;
pickupDate: Date;
deliveryDate: Date;
status: TruckingLegStatus;
distance?: number;
duration?: number;
}

export type TruckingLegStatus =
| "pending"
| "picked_up"
| "in_transit"
| "delivered";

export interface ServiceRepairJob {
_id: string;
client: string;
parentCompany: string;
author: string;
orderNumber: string;
appointmentDate: Date;
serviceType: ServiceType;
location: Location;
technician?: string;
status: ServiceJobStatus;
items?: ServiceItem[];
rates?: ServiceRates;
createdAt: Date;
updatedAt: Date;
}

export type ServiceType = "hvac" | "plumbing" | "electrical" | "general";
export type ServiceJobStatus =
| "scheduled"
| "in_progress"
| "completed"
| "cancelled";

export interface ServiceItem {
_id: string;
name: string;
description: string;
quantity: number;
unitPrice: number;
totalPrice: number;
category: string;
}

// Common types
export interface Location {
address: string;
city: string;
state: string;
zipCode: string;
coordinates?: {
lat: number;
lng: number;
};
}

export interface Client {
_id: string;
name: string;
email: string;
phone?: string;
address: Location;
parentCompany: string;
active: boolean;
slug: string;
rates?: ClientRates;
avatar?: string;
createdAt: Date;
updatedAt: Date;
}

// Rate types
export interface RateSettings {
hourly?: number;
mileage?: number;
overtime?: number;
holiday?: number;
emergency?: number;
}

export interface TruckingRates extends RateSettings {
perMile?: number;
perHour?: number;
fuel?: number;
detention?: number;
}

export interface ServiceRates extends RateSettings {
serviceCall?: number;
diagnostic?: number;
parts?: number;
labor?: number;
}

export interface ClientRates {
hourly?: number;
hourlyRate?: number;
mileage?: number;
overtime?: number;
holiday?: number;
emergency?: number;
}

// Customer configuration types
export interface CustomerConfig {
parentCompany: string;
appType: Industry;
features: FeatureSettings;
rates: RateSettings;
isMobile: boolean;
checkClientTrustStatus: (client: Client) => Promise<boolean>;
getRates: (options: { employeeId?: string; clientId?: string }) => Promise<{
global: RateSettings;
employee: RateSettings;
client: ClientRates;
}>;
}

export interface FeatureSettings {
Job: {
items: {
model: string;
approval: {
enabled: boolean;
autoApproval: {
enabled: boolean;
clientTypes: {
trusted: {
enabled: boolean;
minCompletedJobs: number;
minTimeAsClient: number;
};
};
};
};
};
};
}

// Middleware types
export interface VerifyTokenRequest extends Request {
user?: User;
payload?: User;
}

export interface VerifyAuthRequest extends AuthenticatedRequest {
payload: User;
}

// Error types
export interface APIError {
code: string;
message: string;
details?: any;
statusCode?: number;
}

// Database types
export interface DatabaseConnection {
connect: () => Promise<void>;
disconnect: () => Promise<void>;
upsertIndexes: () => Promise<void>;
}

// Token types
export interface TokenPayload {
id: string;
email: string;
name: string;
authority: UserRole;
type?: string;
parentCompany: string;
tokenType: "access" | "refresh";
jti: string;
employeeId: string;
role: string;
iat?: number;
exp?: number;
}

export interface RefreshTokenDocument {
_id: string;
token: string;
userId: string;
type: "refresh" | "access" | "quote" | "proposal";
jti: string;
previousJti?: string;
expiresAt: Date;
isRevoked: boolean;
createdAt: Date;
updatedAt: Date;
}

// Performance monitoring types
export interface PerformanceLog {
_id: string;
method: string;
url: string;
duration: number;
timestamp: Date;
userId?: string;
statusCode: number;
userAgent?: string;
contentType?: string;
ip?: string;
route?: string;
query?: string;
responseSize?: number;
tenant?: string;
industry?: Industry;
memoryUsage?: number;
requestId?: string;
environment?: string;
}

// Utility types
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type Partial<T> = { [P in keyof T]?: T[P] };
export type Required<T> = { [P in keyof T]-?: T[P] };

// Express extended types
declare global {
namespace Express {
interface Request {
user?: User;
payload?: User;
customerConfig?: CustomerConfig;
tenant?: ParentCompany;
}
}
}

2. Model Type Definitions (types/models.ts)​

import { Document, Schema } from "mongoose";
import {
User,
Client,
ParentCompany,
TruckingJob,
ServiceRepairJob,
Location,
} from "./index";

// Mongoose document interfaces
export interface UserDocument extends User, Document {
generateJWT(options: { type: string; expiresIn: string }): string;
generateAccessToken(): string;
generateRefreshToken(): string;
toJsonWithToken(token: string): User;
toJsonCurrent(): User;
}

export interface ClientDocument extends Client, Document {}

export interface ParentCompanyDocument extends ParentCompany, Document {}

export interface TruckingJobDocument extends TruckingJob, Document {}

export interface ServiceRepairJobDocument extends ServiceRepairJob, Document {}

// Schema type definitions
export interface UserSchema extends Schema {
firstName: string;
lastName: string;
email: string;
hash: string;
authority: string;
active: boolean;
parentCompany: Schema.Types.ObjectId;
type?: string;
company?: string;
meta?: {
role?: string;
permissions?: string[];
};
fullName: string;
}

export interface ClientSchema extends Schema {
name: string;
email: string;
phone?: string;
address: Location;
parentCompany: Schema.Types.ObjectId;
active: boolean;
slug: string;
rates?: any;
avatar?: string;
}

export interface JobSchema extends Schema {
client: Schema.Types.ObjectId;
parentCompany: Schema.Types.ObjectId;
author: Schema.Types.ObjectId;
orderNumber: string;
status: string;
items?: Schema.Types.ObjectId[];
itemsModel: string;
rates?: any;
assignedTo?: Schema.Types.ObjectId[];
useGlobalRates?: boolean;
transactionDate?: Date;
appointmentDate?: Date;
}

// Controller return types
export interface ControllerResponse<T = any> {
status: "success" | "error";
data?: T;
message?: string;
code?: number;
}

// Service layer types
export interface ServiceResult<T = any> {
success: boolean;
data?: T;
error?: string;
statusCode?: number;
}

// Query types
export interface QueryOptions {
page?: number;
limit?: number;
sort?: string;
filter?: any;
search?: string;
}

export interface PaginationOptions {
page: number;
limit: number;
sort?: any;
populate?: string | string[];
}

// API endpoint types
export interface GetJobsQuery {
page?: string;
limit?: string;
status?: string;
client?: string;
assignedTo?: string;
dateFrom?: string;
dateTo?: string;
industry?: string;
}

export interface CreateJobBody {
client: string;
orderNumber?: string;
transactionDate?: string;
appointmentDate?: string;
items?: any[];
assignedTo?: string[];
useGlobalRates?: boolean;
notes?: string;
}

export interface UpdateJobBody {
status?: string;
assignedTo?: string[];
items?: any[];
notes?: string;
useGlobalRates?: boolean;
}

3. Controller Type Definitions (types/controllers.ts)​

import { Request, Response, NextFunction } from "express";
import { AuthenticatedRequest, APIResponse } from "./index";

// Base controller types
export type ControllerFunction = (
req: Request,
res: Response,
next?: NextFunction
) => Promise<void> | void;

export type AuthenticatedControllerFunction = (
req: AuthenticatedRequest,
res: Response,
next?: NextFunction
) => Promise<void> | void;

// Specific controller types
export interface JobController {
getJobs: AuthenticatedControllerFunction;
getJob: AuthenticatedControllerFunction;
createJob: AuthenticatedControllerFunction;
updateJob: AuthenticatedControllerFunction;
deleteJob: AuthenticatedControllerFunction;
updateRates: AuthenticatedControllerFunction;
getTotalJobCountByClient: AuthenticatedControllerFunction;
jobUseGlobalRates: AuthenticatedControllerFunction;
}

export interface ClientController {
getClients: AuthenticatedControllerFunction;
getClient: AuthenticatedControllerFunction;
createClient: AuthenticatedControllerFunction;
updateClient: AuthenticatedControllerFunction;
deleteClient: AuthenticatedControllerFunction;
recentClients: AuthenticatedControllerFunction;
searchClients: AuthenticatedControllerFunction;
getClientMembers: AuthenticatedControllerFunction;
updateRates: AuthenticatedControllerFunction;
}

export interface UserController {
getUsers: AuthenticatedControllerFunction;
getUser: AuthenticatedControllerFunction;
createUser: AuthenticatedControllerFunction;
updateUser: AuthenticatedControllerFunction;
deleteUser: AuthenticatedControllerFunction;
getCurrentUser: AuthenticatedControllerFunction;
}

export interface AuthController {
login: ControllerFunction;
logout: AuthenticatedControllerFunction;
refresh: ControllerFunction;
register: ControllerFunction;
forgotPassword: ControllerFunction;
resetPassword: ControllerFunction;
}

// Response helper types
export interface ResponseHelper {
success<T>(res: Response, data: T, message?: string): void;
error(res: Response, error: string, statusCode?: number): void;
created<T>(res: Response, data: T, message?: string): void;
updated<T>(res: Response, data: T, message?: string): void;
deleted(res: Response, message?: string): void;
notFound(res: Response, message?: string): void;
unauthorized(res: Response, message?: string): void;
forbidden(res: Response, message?: string): void;
validation(res: Response, errors: any): void;
}

// Middleware types
export interface MiddlewareFunction {
(req: Request, res: Response, next: NextFunction): void | Promise<void>;
}

export interface AuthMiddleware {
verifyToken: MiddlewareFunction;
verifyAuth: MiddlewareFunction;
admin: MiddlewareFunction;
owner: MiddlewareFunction;
user: MiddlewareFunction;
client: MiddlewareFunction;
superAdmin: MiddlewareFunction;
}

// Error handling types
export interface ErrorHandler {
(error: Error, req: Request, res: Response, next: NextFunction): void;
}

export interface CustomError extends Error {
statusCode?: number;
code?: string;
details?: any;
}

Phase 3: Gradual File Migration​

1. Start with Utilities (utils/response.ts)​

// Convert existing utils/response.js to TypeScript
import { Response } from "express";
import { APIResponse } from "@/types";

export class ResponseHelper {
static success<T>(res: Response, data: T, message?: string): void {
const response: APIResponse<T> = {
status: "success",
data,
message,
};
res.status(200).json(response);
}

static error(res: Response, error: string, statusCode: number = 500): void {
const response: APIResponse = {
status: "error",
error: {
code: "INTERNAL_ERROR",
message: error,
},
};
res.status(statusCode).json(response);
}

static created<T>(res: Response, data: T, message?: string): void {
const response: APIResponse<T> = {
status: "success",
data,
message: message || "Resource created successfully",
};
res.status(201).json(response);
}

static updated<T>(res: Response, data: T, message?: string): void {
const response: APIResponse<T> = {
status: "success",
data,
message: message || "Resource updated successfully",
};
res.status(200).json(response);
}

static deleted(res: Response, message?: string): void {
const response: APIResponse = {
status: "success",
message: message || "Resource deleted successfully",
};
res.status(200).json(response);
}

static notFound(res: Response, message?: string): void {
const response: APIResponse = {
status: "error",
error: {
code: "NOT_FOUND",
message: message || "Resource not found",
},
};
res.status(404).json(response);
}

static unauthorized(res: Response, message?: string): void {
const response: APIResponse = {
status: "error",
error: {
code: "UNAUTHORIZED",
message: message || "Unauthorized access",
},
};
res.status(401).json(response);
}

static forbidden(res: Response, message?: string): void {
const response: APIResponse = {
status: "error",
error: {
code: "FORBIDDEN",
message: message || "Access forbidden",
},
};
res.status(403).json(response);
}

static validation(res: Response, errors: any): void {
const response: APIResponse = {
status: "error",
error: {
code: "VALIDATION_ERROR",
message: "Validation failed",
details: errors,
},
};
res.status(400).json(response);
}
}

2. Convert Constants (constants/index.ts)​

// Convert existing constants/index.js to TypeScript
import { UserRole } from "@/types";

interface GeneralConstants {
TOKEN_NAME: string;
REFRESH_TOKEN_NAME: string;
TOKEN_EXPIRE: string;
REFRESH_TOKEN_EXPIRES_WEB: string;
REFRESH_TOKEN_EXPIRES_MOBILE: string;
CREATE_USER_EXPIRATION: string;
TOKEN_EXPIRE_TTL: number;
REFRESH_TOKEN_EXPIRES_WEB_TTL: number;
REFRESH_TOKEN_EXPIRES_MOBILE_TTL: number;
HEADER_CLIENT_TYPE: string;
HEADER_APP_TYPE: string;
HEADER_PARENT_COMPANY: string;
HEADER_CUSTOMER_CONTEXT: string;
REGISTRATION_HEADER_NAME: string;
CLIENT_HEADER_NAME: string;
APPROVAL_TOKEN_EXPIRATION: string;
APPROVAL_TOKEN_EXPIRATION_TTL: number;
}

interface MessagesConstants {
USER_HAS_PERMISSION: string;
}

interface ErrorConstants {
UNKNOWN: string;
PROVIDE_PASSWORD: string;
USER_NOT_PERMITTED: string;
}

interface StatusConstants {
INFO: string;
SUCCESS: string;
WARN: string;
ERROR: string;
}

interface RolesConstants {
CLIENT: UserRole;
USER: UserRole;
ADMIN: UserRole;
OWNER: UserRole;
SUPER: UserRole;
}

interface Constants {
general: GeneralConstants;
messages: MessagesConstants;
error: ErrorConstants;
status: StatusConstants;
roles: RolesConstants;
}

const { stage } = require("../config");

export const constants: Constants = {
general: {
TOKEN_NAME: `Attune-${stage}-Context`,
REFRESH_TOKEN_NAME: `Attune-${stage}-Refresh`,
TOKEN_EXPIRE: "4h",
REFRESH_TOKEN_EXPIRES_WEB: "12h",
REFRESH_TOKEN_EXPIRES_MOBILE: "10d",
CREATE_USER_EXPIRATION: "2d",
TOKEN_EXPIRE_TTL: 4 * 60 * 60 * 1000,
REFRESH_TOKEN_EXPIRES_WEB_TTL: 12 * 60 * 60 * 1000,
REFRESH_TOKEN_EXPIRES_MOBILE_TTL: 10 * 24 * 60 * 60 * 1000,
HEADER_CLIENT_TYPE: "x-client-type",
HEADER_APP_TYPE: "x-app-type",
HEADER_PARENT_COMPANY: "x-parent-company",
HEADER_CUSTOMER_CONTEXT: "customer-context",
REGISTRATION_HEADER_NAME: "x-registration-token-id",
CLIENT_HEADER_NAME: "x-client-token-id",
APPROVAL_TOKEN_EXPIRATION: "7d",
APPROVAL_TOKEN_EXPIRATION_TTL: 7 * 24 * 60 * 60 * 1000,
},

messages: {
USER_HAS_PERMISSION: "User has permission",
},

error: {
UNKNOWN: "Something went wrong",
PROVIDE_PASSWORD: "Please provide a password",
USER_NOT_PERMITTED: "User does not have permission",
},

status: {
INFO: "info",
SUCCESS: "success",
WARN: "warning",
ERROR: "error",
},

roles: {
CLIENT: "client",
USER: "user",
ADMIN: "admin",
OWNER: "owner",
SUPER: "superAdmin",
},
};

// Export individual constants for backward compatibility
export const { general, messages, error, status, roles } = constants;

// Default export for backward compatibility
export default constants;

3. Convert a Model (models/User.ts)​

// Convert existing models/User.js to TypeScript
import mongoose, { Schema, Document } from "mongoose";
import jwt from "jsonwebtoken";
import { User, UserDocument, TokenPayload } from "@/types";
import { constants } from "@/constants";
import { COOKIE_KEY, REFRESH_KEY } from "@/config/keys";

const { general } = constants;

const UserSchema = new Schema<UserDocument>(
{
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
hash: {
type: String,
required: true,
},
authority: {
type: String,
enum: ["client", "user", "admin", "owner", "superAdmin"],
default: "user",
},
active: {
type: Boolean,
default: true,
},
parentCompany: {
type: Schema.Types.ObjectId,
ref: "ParentCompany",
required: true,
},
type: {
type: String,
},
company: {
type: String,
},
meta: {
role: String,
permissions: [String],
},
},
{
timestamps: true,
}
);

// Virtual for full name
UserSchema.virtual("fullName").get(function (this: UserDocument) {
return `${this.firstName} ${this.lastName}`;
});

// Methods
UserSchema.methods.generateJWT = function (
this: UserDocument,
optionsObj: { type: string; expiresIn: string }
) {
const options = { ...optionsObj };
const signObj: Partial<TokenPayload> = {
id: this.id,
email: this.email,
name: this.fullName,
authority: this.authority,
type: this.type,
company: this.company,
parentCompany: this.parentCompany,
tokenType: options.type as "access" | "refresh",
jti: new mongoose.Types.ObjectId().toString(),
};

const signOptions = {
expiresIn: options.expiresIn,
};

if (options.type === "register") {
signObj.hash = this.hash;
delete signObj.name;
}

const secret = options.type === "refresh" ? REFRESH_KEY : COOKIE_KEY;
return jwt.sign(signObj, secret, signOptions);
};

UserSchema.methods.generateAccessToken = function (this: UserDocument): string {
return jwt.sign(
{
id: this._id,
email: this.email,
name: this.fullName,
authority: this.authority,
type: this.type,
parentCompany: this.parentCompany,
tokenType: "access",
jti: new mongoose.Types.ObjectId().toString(),
employeeId: this._id,
role: this.meta?.role || this.authority,
},
COOKIE_KEY,
{ expiresIn: general.TOKEN_EXPIRE }
);
};

UserSchema.methods.generateRefreshToken = function (
this: UserDocument
): string {
return jwt.sign(
{
id: this._id,
email: this.email,
name: this.fullName,
authority: this.authority,
type: this.type,
parentCompany: this.parentCompany,
tokenType: "refresh",
jti: new mongoose.Types.ObjectId().toString(),
employeeId: this._id,
role: this.meta?.role || this.authority,
},
REFRESH_KEY,
{ expiresIn: general.REFRESH_TOKEN_EXPIRES_WEB }
);
};

UserSchema.methods.toJsonWithToken = function (
this: UserDocument,
token: string
): User {
const finalUser = this.toJSON();
delete finalUser.hash;
(finalUser as any).apiTok = token;
return finalUser;
};

UserSchema.methods.toJsonCurrent = function (this: UserDocument): User {
const finalUser = this.toJSON();
delete finalUser.hash;
return finalUser;
};

// Export both for backward compatibility
export const User = mongoose.model<UserDocument>("User", UserSchema);
export default User;

4. Convert a Controller (controllers/users/index.ts)​

// Convert existing controllers/users/index.js to TypeScript
import { Response } from "express";
import { User } from "@/models/User";
import { AuthenticatedRequest, APIResponse, UserDocument } from "@/types";
import { ResponseHelper } from "@/utils/response";

interface UserController {
getUsers: (req: AuthenticatedRequest, res: Response) => Promise<void>;
getUser: (req: AuthenticatedRequest, res: Response) => Promise<void>;
createUser: (req: AuthenticatedRequest, res: Response) => Promise<void>;
updateUser: (req: AuthenticatedRequest, res: Response) => Promise<void>;
deleteUser: (req: AuthenticatedRequest, res: Response) => Promise<void>;
getCurrentUser: (req: AuthenticatedRequest, res: Response) => Promise<void>;
}

export const userController: UserController = {
getUsers: async (req: AuthenticatedRequest, res: Response): Promise<void> => {
try {
const { parentCompany } = req.payload;
const { page = 1, limit = 20, search = "" } = req.query;

const query: any = { parentCompany };

if (search) {
query.$or = [
{ firstName: { $regex: search, $options: "i" } },
{ lastName: { $regex: search, $options: "i" } },
{ email: { $regex: search, $options: "i" } },
];
}

const users = await User.find(query)
.select("-hash")
.limit(Number(limit))
.skip((Number(page) - 1) * Number(limit))
.sort({ createdAt: -1 });

const totalCount = await User.countDocuments(query);

ResponseHelper.success(res, {
users,
totalCount,
totalPages: Math.ceil(totalCount / Number(limit)),
currentPage: Number(page),
});
} catch (error) {
ResponseHelper.error(res, error.message);
}
},

getUser: async (req: AuthenticatedRequest, res: Response): Promise<void> => {
try {
const { id } = req.params;
const { parentCompany } = req.payload;

const user = await User.findOne({ _id: id, parentCompany }).select(
"-hash"
);

if (!user) {
ResponseHelper.notFound(res, "User not found");
return;
}

ResponseHelper.success(res, user);
} catch (error) {
ResponseHelper.error(res, error.message);
}
},

createUser: async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { parentCompany } = req.payload;
const userData = { ...req.body, parentCompany };

const user = new User(userData);
await user.save();

ResponseHelper.created(res, user.toJsonCurrent());
} catch (error) {
ResponseHelper.error(res, error.message);
}
},

updateUser: async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { id } = req.params;
const { parentCompany } = req.payload;
const updateData = req.body;

const user = await User.findOneAndUpdate(
{ _id: id, parentCompany },
updateData,
{ new: true, runValidators: true }
).select("-hash");

if (!user) {
ResponseHelper.notFound(res, "User not found");
return;
}

ResponseHelper.updated(res, user);
} catch (error) {
ResponseHelper.error(res, error.message);
}
},

deleteUser: async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { id } = req.params;
const { parentCompany } = req.payload;

const user = await User.findOneAndDelete({ _id: id, parentCompany });

if (!user) {
ResponseHelper.notFound(res, "User not found");
return;
}

ResponseHelper.deleted(res, "User deleted successfully");
} catch (error) {
ResponseHelper.error(res, error.message);
}
},

getCurrentUser: async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { id } = req.payload;

const user = await User.findById(id).select("-hash");

if (!user) {
ResponseHelper.notFound(res, "User not found");
return;
}

ResponseHelper.success(res, user);
} catch (error) {
ResponseHelper.error(res, error.message);
}
},
};

// Export individual functions for backward compatibility
export const {
getUsers,
getUser,
createUser,
updateUser,
deleteUser,
getCurrentUser,
} = userController;

// Default export for backward compatibility
export default userController;

Phase 4: Build and Development Process​

1. Development Scripts​

# Development with TypeScript
npm run dev:ts

# Build TypeScript to JavaScript
npm run build

# Type checking without compilation
npm run type-check

# Watch mode for type checking
npm run build:watch

2. Update CI/CD Pipeline​

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Type checking
run: npm run type-check

- name: Run tests (JavaScript)
run: npm test

- name: Run tests (TypeScript)
run: npm run test:ts

- name: Build TypeScript
run: npm run build

- name: Deploy
if: github.ref == 'refs/heads/main'
run: |
# Your deployment script here
echo "Deploying to production..."

3. VSCode Configuration (.vscode/settings.json)​

{
"typescript.preferences.includePackageJsonAutoImports": "auto",
"typescript.suggest.autoImports": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.fixAll.eslint": true
},
"files.associations": {
"*.js": "javascript",
"*.ts": "typescript"
},
"search.exclude": {
"dist/**": true,
"build/**": true,
"node_modules/**": true
}
}

Backward Compatibility Strategies​

1. Keep Existing JavaScript Files​

// Your existing JavaScript files continue to work
// No changes needed to existing controllers, models, etc.
// TypeScript will provide type checking for new files

2. Gradual Migration Path​

// Step 1: Add types to existing JavaScript files
// Step 2: Rename .js to .ts gradually
// Step 3: Add proper TypeScript features
// Step 4: Enable strict mode incrementally

3. Mixed Codebase Support​

// TypeScript files can import JavaScript files
import { existingFunction } from "./existing-file.js";

// JavaScript files can use TypeScript files (after compilation)
const { newFunction } = require("./new-file.js"); // compiled from .ts

Testing Strategy​

1. Test Configuration​

// tests/setup.ts
import { MongoMemoryServer } from "mongodb-memory-server";
import mongoose from "mongoose";

let mongoServer: MongoMemoryServer;

beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
});

afterAll(async () => {
await mongoose.connection.close();
await mongoServer.stop();
});

beforeEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});

2. TypeScript Test Example​

// tests/controllers/users.test.ts
import request from "supertest";
import app from "../../app";
import { User } from "../../models/User";
import { AuthenticatedRequest } from "../../types";

describe("User Controller", () => {
let authToken: string;
let userId: string;

beforeEach(async () => {
// Setup test data
const user = new User({
firstName: "John",
lastName: "Doe",
email: "john@example.com",
hash: "hashedpassword",
parentCompany: "company123",
});
await user.save();

authToken = user.generateAccessToken();
userId = user._id.toString();
});

describe("GET /users", () => {
it("should return users for authenticated user", async () => {
const response = await request(app)
.get("/api/v1/users")
.set("Authorization", `Bearer ${authToken}`)
.expect(200);

expect(response.body.status).toBe("success");
expect(response.body.data.users).toBeDefined();
expect(Array.isArray(response.body.data.users)).toBe(true);
});

it("should return 401 for unauthenticated user", async () => {
await request(app).get("/api/v1/users").expect(401);
});
});

describe("GET /users/:id", () => {
it("should return specific user", async () => {
const response = await request(app)
.get(`/api/v1/users/${userId}`)
.set("Authorization", `Bearer ${authToken}`)
.expect(200);

expect(response.body.status).toBe("success");
expect(response.body.data._id).toBe(userId);
});

it("should return 404 for non-existent user", async () => {
const fakeId = "507f1f77bcf86cd799439011";

await request(app)
.get(`/api/v1/users/${fakeId}`)
.set("Authorization", `Bearer ${authToken}`)
.expect(404);
});
});
});

Migration Checklist​

Phase 1: Setup βœ…β€‹

  • Install TypeScript dependencies
  • Configure tsconfig.json
  • Update package.json scripts
  • Configure Jest for TypeScript
  • Test basic TypeScript compilation

Phase 2: Type Definitions βœ…β€‹

  • Create core type definitions
  • Define model interfaces
  • Create controller types
  • Add middleware types
  • Define API response types

Phase 3: File Migration βœ…β€‹

  • Convert utilities first
  • Migrate constants
  • Convert models one by one
  • Update controllers gradually
  • Migrate middleware functions

Phase 4: Testing & Deployment βœ…β€‹

  • Update CI/CD pipeline
  • Configure development environment
  • Test mixed JavaScript/TypeScript codebase
  • Deploy with backward compatibility
  • Monitor for any issues

Benefits of This Approach​

1. Zero Downtime Migration​

  • Existing JavaScript code continues to work
  • No need to rewrite everything at once
  • Gradual adoption reduces risk

2. Improved Developer Experience​

  • Better IDE support and autocomplete
  • Catch errors at compile time
  • Improved code documentation

3. Better Code Quality​

  • Type safety prevents runtime errors
  • Better refactoring capabilities
  • Improved maintainability

4. Industry-Specific Types​

  • Trucking and service industry types
  • Better API documentation
  • Improved integration experience

Next Steps​

  1. Phase 1: Set up TypeScript infrastructure (this week)
  2. Phase 2: Create type definitions (next week)
  3. Phase 3: Start migrating utilities and models
  4. Phase 4: Gradually convert controllers and services

This approach ensures 100% backward compatibility while providing a clear path to full TypeScript adoption. Your existing production code remains untouched while new code benefits from TypeScript's type safety and tooling.

Would you like me to help you implement any specific phase of this migration strategy?