We recently migrated our 200k+ line TypeScript codebase from 4.9 to 5.0. Here's what we learned and the issues you'll likely encounter.
Breaking Changes That Actually Matter
TypeScript 5.0 isn't as breaking as the major version suggests, but there are some gotchas:
1. Stricter Function Type Checking
This code worked in 4.9 but fails in 5.0:
1234567// TypeScript 4.9: This was allowedfunction process(items: string[]) {return items.map(item => item.length)}const result: number[] = process(['a', 'b', null]) // null was silently ignored// TypeScript 5.0: Now correctly errors// Argument of type '(string | null)[]' is not assignable to parameter of type 'string[]'
Fix: Add proper null checking or update your types:
12345function process(items: (string | null)[]) {return items.filter((item): item is string => item !== null).map(item => item.length)}
New Features Worth Using
Decorators (Stage 3)
Finally, standard decorators! We migrated from experimental decorators in our NestJS backend:
1234567891011121314151617// Before (experimental decorators)import { Injectable, Get } from '@nestjs/common'@Injectable()export class UserService {@Get()async getUsers() {return this.userRepository.find()}}// After (standard decorators) - same syntax, better performance// Just update your tsconfig.json:{"compilerOptions": {"experimentalDecorators": false, // Remove this"emitDecoratorMetadata": true}}
const Type Parameters
This solves a common React props inference problem:
123456789101112// Before: Type inference was too widefunction createOptions(options: T[]) { return options}const fruits = createOptions(['apple', 'banana']) // Type: string[] (too wide!)// After: Use const type parametersfunction createOptions(options: T[]) { return options}const fruits = createOptions(['apple', 'banana'])// Type: readonly ['apple', 'banana'] (exact!)
Migration Steps
1. Update Dependencies
1234npm update typescript@latestnpm update @types/node@latestnpm update @typescript-eslint/parser@latestnpm update @typescript-eslint/eslint-plugin@latest
2. Fix tsconfig.json
123456789{"compilerOptions": {"target": "ES2022", // Update from ES2020"lib": ["ES2023"], // Add new lib features"moduleResolution": "bundler", // New resolution strategy"allowImportingTsExtensions": true, // For Bun/Deno"verbatimModuleSyntax": true // Clearer import/export}}
3. Address Type Errors
Run tsc --noEmit
and fix errors systematically. Most common issues:
- Enum changes: String enums are more strict
- lib.d.ts updates: Some DOM types changed
- Bundler resolution: Import statements need file extensions in some cases
Performance Improvements
Our compilation times improved by 23% after the migration:
- Before: 45s full build, 12s incremental
- After: 35s full build, 8s incremental
Real Migration Timeline
For our team of 8 developers:
- Day 1: Update dependencies, fix tsconfig
- Day 2-3: Fix type errors (mostly enum-related)
- Day 4: Update CI/CD pipeline
- Day 5: Deploy to staging, fix edge cases
Gotchas to Watch For
ESLint Configuration
Update your ESLint parser or you'll get strange errors:
12345678// .eslintrc.jsmodule.exports = {parser: '@typescript-eslint/parser',parserOptions: {ecmaVersion: 2023, // Update thisproject: './tsconfig.json'}}
TypeScript 5.0 is a solid upgrade with meaningful performance improvements. The breaking changes are manageable, and the new features make the migration worthwhile.