Menu

Migrating to TypeScript 5.0: What Changed and Why It Matters
May 25, 2025Development10 min read

Migrating to TypeScript 5.0: What Changed and Why It Matters

D
Dev team

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:

text
1
2
3
4
5
6
7
// TypeScript 4.9: This was allowed
function 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:

text
1
2
3
4
5
function 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:

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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:

text
1
2
3
4
5
6
7
8
9
10
11
12
// Before: Type inference was too wide
function createOptions(options: T[]) {
return options
}
const fruits = createOptions(['apple', 'banana'])
// Type: string[] (too wide!)
// After: Use const type parameters
function createOptions(options: T[]) {
return options
}
const fruits = createOptions(['apple', 'banana'])
// Type: readonly ['apple', 'banana'] (exact!)

Migration Steps

1. Update Dependencies

text
1
2
3
4
npm update typescript@latest
npm update @types/node@latest
npm update @typescript-eslint/parser@latest
npm update @typescript-eslint/eslint-plugin@latest

2. Fix tsconfig.json

text
1
2
3
4
5
6
7
8
9
{
"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:

text
1
2
3
4
5
6
7
8
// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2023, // Update this
project: './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.

Share this article