TypeScript: From 'Why Bother?' to 'Can't Live Without It'
How writing thousands of lines of JavaScript taught me why TypeScript is non-negotiable for any serious project
TypeScript: From 'Why Bother?' to 'Can't Live Without It'
🤦 The JavaScript Honeymoon Phase
Started my coding journey with JavaScript. Loved it. So flexible! So forgiving! No compilation step! Just write code and it runs!
Then I built Four-Points - my hotel management system. Started in pure JavaScript. 5,000+ lines later, I learned a painful truth:
Flexibility in JavaScript means "bugs will appear in production."
The classic dev journey:
- "Which language should I learn?" → Months of research
- Experts: "It depends on your goals"
- You: "Nah, I want the BEST one"
- Finally pick JavaScript
- Hear about TypeScript: "Nah, I already know JS"
- Start building real stuff, migrate time comes
- Reality check: You'll never stop learning in this career
Get used to it. We all went through this. 💀
💀 The JavaScript Pain Points
1. The Runtime Roulette
// This looks fine...
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0)
}
// Until someone passes this:
calculateTotal(undefined) // 💥 Cannot read property 'reduce' of undefined
calculateTotal([{ name: 'Coffee' }]) // 💥 NaN (no price property)
calculateTotal('wrong') // 💥 'wrong'[Symbol.iterator] is not a function
Every function is a landmine. You don't know it breaks until it actually runs. In production. With real users.
2. The Refactoring Nightmare
// Rename this property across 50 files
const user = {
userName: 'john', // Changed to 'username'
email: 'john@example.com'
}
// Find all places using 'userName' manually
// Miss one reference? Runtime error in production
// Good luck
No IDE can reliably refactor JavaScript. Find & replace? Hope you don't have a variable named userName that isn't the user object.
3. The Autocomplete Guessing Game
// What properties does this object have?
function updateUser(user) {
user. // ← IDE shows... nothing helpful
}
Your IDE can't help you. You need to either:
- Remember what properties exist
- Look at the code where the object is created
- Console.log() and inspect
- Just try and see what breaks
4. The Documentation Lie
/**
* Fetches user data
* @param {number} userId - The user ID
* @returns {Object} User object
*/
async function getUser(userId) {
// Implementation
}
// 6 months later, code changed but docs didn't
// Now accepts string IDs
// Returns null on error
// Documentation is lying
// You trust it anyway
// 💥
JSDoc comments are better than nothing, but they're not enforced. Code changes, docs don't. Now they're just lies.
5. The "It Works on My Machine" Mystery
// Dev sends this to API:
const data = {
userId: 123,
amount: '50.00' // Oops, string instead of number
}
// Backend expects:
{
userId: number,
amount: number
}
// Works in dev (loose comparison)
// Breaks in prod (strict validation)
// No idea why until you debug for 2 hours, best case, or ruins the project on porpose
🚀 The TypeScript Awakening
After the 47th runtime error that TypeScript would've caught, I migrated Four-Points to TypeScript. 2 weeks of pain. Totally worth it.
What Changed Immediately
Before (JavaScript):
function createBooking(data) {
return fetch('/api/bookings', {
method: 'POST',
body: JSON.stringify(data)
})
}
// Call it with wrong data? Find out in production
createBooking({
guestName: 'John',
romNumber: 101 // Typo: should be 'roomNumber'
})
After (TypeScript):
interface Booking {
guestName: string
roomNumber: number
checkIn: Date
checkOut: Date
}
function createBooking(data: Booking): Promise<Response> {
return fetch('/api/bookings', {
method: 'POST',
body: JSON.stringify(data)
})
}
// Try to pass wrong data:
createBooking({
guestName: 'John',
romNumber: 101 // ← Red squiggly line BEFORE you run
})
// Error: Object literal may only specify known properties,
// and 'romNumber' does not exist in type 'Booking'
Caught the typo before even running the code. This alone pays for TypeScript.
💡 Best Practices I Learned the Hard Way
1. Type Everything, Even When Obvious
Bad:
// "It's obvious this returns a string"
function getUserName(user) {
return user.name
}
Good:
interface User {
name: string
email: string
}
function getUserName(user: User): string {
return user.name
}
Future you will thank present you. Trust me.
2. Avoid any Like the Plague
// Might as well use JavaScript
function processData(data: any): any {
// TypeScript can't help you now
}
// Actually type it
interface InputData {
id: number
values: string[]
}
interface ProcessedData {
id: number
result: number
}
function processData(data: InputData): ProcessedData {
// Now TypeScript has your back
}
Every any is a potential runtime error. If you're using any, you're not using TypeScript.
3. Use Strict Mode
// tsconfig.json
{
"compilerOptions": {
"strict": true, // Non-negotiable
"strictNullChecks": true,
"noImplicitAny": true,
"strictFunctionTypes": true
}
}
Strict mode is annoying until it saves you from a production bug. Then it's your best friend.
4. Interfaces for Objects, Types for Everything Else
// Use interfaces for objects
interface User {
id: number
name: string
email: string
}
// Use types for unions, intersections, utilities
type Status = 'pending' | 'active' | 'inactive'
type UserWithStatus = User & { status: Status }
type PartialUser = Partial<User>
5. Don't Fight the Type System
If TypeScript is complaining, there's probably a real problem:
// TypeScript: "Hey, this might be undefined"
function getFirstItem<T>(items: T[]): T {
return items[0] // ← What if array is empty?
}
// Listen to TypeScript
function getFirstItem<T>(items: T[]): T | undefined {
return items[0] // ← Now handles empty arrays correctly
}
6. Use Unknown Instead of Any
// Bad
function parseJSON(json: string): any {
return JSON.parse(json)
}
// Good
function parseJSON<T>(json: string): unknown {
return JSON.parse(json)
}
// Forces you to validate before using
const data = parseJSON(jsonString)
if (isUser(data)) { // Type guard
console.log(data.name) // Now safe
}
7. Create Utility Types for Common Patterns
// API Response wrapper
type ApiResponse<T> = {
data: T | null
error: string | null
loading: boolean
}
// Usage
const userResponse: ApiResponse<User> = {
data: { id: 1, name: 'John' },
error: null,
loading: false
}
🎯 Real-World Impact on Four-Points
Before TypeScript:
- ~15 runtime errors per week
- 2-3 hours debugging type-related issues
- Fear of refactoring
- "It works in dev" surprises in prod
After TypeScript:
- ~2 runtime errors per week (and they're real logic bugs)
- Most errors caught during development
- Confident refactoring with IDE support
- API contracts enforced at compile time
Specific Wins:
1. Cashier Module
interface CashierShift {
id: number
userId: number
startTime: Date
endTime: Date | null
denominations: Denomination[]
totalCash: number
status: 'open' | 'closed'
}
// Can't accidentally pass wrong status
// Can't forget required fields
// Can't mix up types
2. API Integration
// Backend contract
interface CreateBookingRequest {
roomNumber: number
guestName: string
checkIn: string // ISO date
checkOut: string // ISO date
}
// Frontend calls
async function createBooking(data: CreateBookingRequest) {
// TypeScript ensures we send the right shape
}
3. Zustand State
interface AppState {
user: User | null
bookings: Booking[]
loading: boolean
error: string | null
// Typed actions
setUser: (user: User) => void
addBooking: (booking: Booking) => void
clearError: () => void
}
// Store is fully typed
const useStore = create<AppState>((set) => ({
// Implementation
}))
🚧 Common Mistakes to Avoid
1. Type Assertions Everywhere
// Bad - bypassing type safety
const user = data as User
const id = value as number
// Good - validate then narrow
if (isUser(data)) {
const user = data // TypeScript knows it's User
}
2. Not Using Generics
// Bad - duplicating logic
function getFirstString(arr: string[]): string | undefined {
return arr[0]
}
function getFirstNumber(arr: number[]): number | undefined {
return arr[0]
}
// Good - one function, all types
function getFirst<T>(arr: T[]): T | undefined {
return arr[0]
}
3. Ignoring Errors with @ts-ignore
// Bad - hiding real problems
// @ts-ignore
const result = riskyOperation()
// Good - fix the actual issue
const result: ResultType = riskyOperation()
📊 Is TypeScript Worth the Learning Curve?
Short answer: Absolutely yes.
Long answer:
Pros:
- Catches bugs before runtime
- Excellent IDE support (autocomplete, refactoring)
- Self-documenting code
- Easier to onboard new developers
- Confidence when refactoring
- Better collaboration (clear contracts)
Cons:
- Initial learning curve
- Sometimes fights you on valid code
- Compilation step (minimal in modern setups)
- More verbose (but that's actually good)
The "cons" disappear after a week. The pros compound forever.
🎬 My Advice
For New Projects
Start with TypeScript. Don't debate. Don't "try JavaScript first." Just use TypeScript from line 1.
For Existing Projects
Migrate gradually:
- Rename
.jsto.ts - Fix errors one file at a time
- Start with strict mode OFF
- Enable strict mode file by file
- Celebrate when everything is typed
For Learning
If you're learning JavaScript, still learn TypeScript:
- Understand JavaScript fundamentals first (1-2 months)
- Add TypeScript immediately after
- Never go back to pure JavaScript for real projects
💭 Final Thoughts
I spent 6 months writing JavaScript before switching to TypeScript. Those 6 months taught me why TypeScript exists. Every bug I fixed would've been caught by TypeScript. Every refactor I feared would've been easy with TypeScript.
TypeScript isn't about being hardcore or following trends. It's about shipping reliable code and sleeping well at night.
If your project will be more than a weekend experiment, use TypeScript. Your future self will thank you.
Keep shipping. Type safely. 🚀
P.S.: Yes, I know some people build huge projects in JavaScript just fine. They're either much better developers than me, or they haven't hit the pain points yet. Don't wait for the pain. Use TypeScript.