TypeScript 5 7

Release Overview

TypeScript 5.7 was released on January 23, 2025, introducing path rewriting improvements for monorepos, checks for never-initialized variables, and refined JSON import attributes support.

Key Metrics:

  • Release Date: January 23, 2025
  • Major Focus: Path rewriting, variable initialization checks, import attributes
  • Breaking Changes: Minimal (stricter error detection)
  • Performance: Better module resolution in complex monorepo setups

Path Rewriting for Relative Paths with rootDirs

Monorepo Feature: TypeScript now correctly rewrites relative import paths when using rootDirs option in monorepo configurations.

The Problem It Solves

Before: Relative imports across rootDirs boundaries required manual path adjustments.

// tsconfig.json
{
  "compilerOptions": {
    "rootDirs": ["src", "generated"]
  }
}

// File structure:
// src/
//   app.ts
// generated/
//   types.ts

// src/app.ts - Before (broken)
import { Types } from "../generated/types";
// ❌ Error: Cannot find module '../generated/types'
// Path doesn't account for rootDirs

// Workaround - absolute imports
import { Types } from "types";
// ✅ Works but loses locality information

With Path Rewriting

Solution: TypeScript automatically rewrites relative paths across rootDirs boundaries.

// src/app.ts - After (works)
import { Types } from "../generated/types";
// ✅ TypeScript rewrites path based on rootDirs
// Compiler understands both src/ and generated/ are roots

// Preserves relative import benefits:
// - Clear import source location
// - Better refactoring support
// - IDE navigation works correctly

Real-World Application: Generated Code Integration

Seamless integration of generated code in build pipelines:

// tsconfig.json
{
  "compilerOptions": {
    "rootDirs": ["src", "generated", "proto-gen"],
    "outDir": "dist"
  }
}

// File structure:
// src/
//   services/
//     user-service.ts
// generated/
//   graphql-types.ts
// proto-gen/
//   api.ts

// src/services/user-service.ts
import { GraphQLTypes } from "../../generated/graphql-types";
// ✅ Path rewritten to account for rootDirs

import { APIClient } from "../../proto-gen/api";
// ✅ Works across all rootDirs

export class UserService {
  async getUser(id: string): Promise<GraphQLTypes.User> {
    const client = new APIClient();
    const response = await client.fetchUser(id);
    return response.data;
  }
}

// Benefits:
// - Generated code separate from source
// - Clean imports without complex path mapping
// - Refactoring preserves import relationships

Real-World Application: Multi-Package Monorepo

Type-safe imports across monorepo packages:

// tsconfig.base.json
{
  "compilerOptions": {
    "rootDirs": [
      "packages/core/src",
      "packages/ui/src",
      "packages/utils/src"
    ]
  }
}

// packages/ui/src/components/Button.tsx
import { theme } from "../../../utils/src/theme";
// ✅ Path rewritten across package boundaries

import { useStore } from "../../../core/src/store";
// ✅ TypeScript handles complex relative paths

export function Button() {
  const state = useStore();
  return <button style={theme.button}>{state.label}</button>;
}

// Enables:
// - Type-safe cross-package imports
// - Refactoring across packages
// - Clear dependency tracking

Real-World Application: Development/Production Code Separation

// tsconfig.json
{
  "compilerOptions": {
    "rootDirs": ["src", "src-dev", "src-prod"]
  }
}

// src-dev/config.ts (development)
export const API_URL = "http://localhost:3000";
export const DEBUG = true;

// src-prod/config.ts (production)
export const API_URL = "https://api.example.com";
export const DEBUG = false;

// src/app.ts
import { API_URL, DEBUG } from "../src-dev/config";
// ✅ Development build uses src-dev/
// ✅ Production build uses src-prod/
// ✅ Same import path, different rootDir selected

if (DEBUG) {
  console.log(`Connecting to ${API_URL}`);
}

Checks for Never-Initialized Variables

Safety Feature: TypeScript now detects variables that are declared but never assigned a value before use.

The Problem It Solves

Before: Variables used before initialization went undetected in certain scenarios.

// Before - bug not caught
function processData() {
  let result: string;

  if (Math.random() > 0.5) {
    result = "success";
  }

  // ❌ BUG: result might be uninitialized
  console.log(result.toUpperCase());
  // Runtime error if Math.random() <= 0.5
}

function calculateTotal(items: number[]) {
  let total: number;

  // ❌ BUG: forgot to initialize total
  for (const item of items) {
    total += item;
    // NaN on first iteration (total is undefined)
  }

  return total;
}

With Never-Initialized Checks

Solution: TypeScript detects variables used without initialization.

// After - errors prevent bugs
function processData() {
  let result: string;

  if (Math.random() > 0.5) {
    result = "success";
  }

  console.log(result.toUpperCase());
  // ❌ Error: Variable 'result' is used before being assigned
}

function calculateTotal(items: number[]) {
  let total: number;

  for (const item of items) {
    total += item;
    // ❌ Error: Variable 'total' is used before being assigned
  }

  return total;
}

// ✅ Correct patterns
function processData() {
  let result: string = "default"; // Initialize
  // OR
  let result: string | undefined; // Explicit undefined

  if (Math.random() > 0.5) {
    result = "success";
  }

  if (result !== undefined) {
    // ✅ Safe: checked before use
    console.log(result.toUpperCase());
  }
}

function calculateTotal(items: number[]) {
  let total: number = 0; // ✅ Initialize

  for (const item of items) {
    total += item; // ✅ Safe: total is initialized
  }

  return total;
}

Real-World Application: Form Validation

Prevent uninitialized validation state bugs:

interface ValidationResult {
  valid: boolean;
  errors: string[];
}

// Before - bug
function validateForm(data: FormData): ValidationResult {
  let result: ValidationResult;

  if (data.email) {
    result = { valid: true, errors: [] };
  }

  // ❌ Error: result used before being assigned
  return result;
  // BUG: result undefined if !data.email
}

// After - fixed
function validateForm(data: FormData): ValidationResult {
  let result: ValidationResult = { valid: false, errors: [] };
  // ✅ Initialize with default

  if (data.email && isValidEmail(data.email)) {
    result = { valid: true, errors: [] };
  } else {
    result.errors.push("Invalid email");
  }

  return result; // ✅ Safe: always initialized
}

Real-World Application: Async Data Loading

Catch uninitialized state in async operations:

// Before - bug
async function loadUser(id: string) {
  let user: User;
  let error: Error;

  try {
    const response = await fetch(`/api/users/${id}`);
    user = await response.json();
  } catch (e) {
    error = e as Error;
  }

  if (error) {
    // ❌ Error: user used before being assigned
    console.error(`Failed to load user ${user.id}`);
    // BUG: user undefined in error path
  }

  return user;
  // ❌ Error: user might be uninitialized
}

// After - fixed
async function loadUser(id: string): Promise<User> {
  let user: User | undefined;
  let error: Error | undefined;

  try {
    const response = await fetch(`/api/users/${id}`);
    user = await response.json();
  } catch (e) {
    error = e as Error;
  }

  if (error) {
    // ✅ Don't access user in error path
    console.error(`Failed to load user ${id}`);
    throw error;
  }

  if (!user) {
    throw new Error("User not loaded");
  }

  return user; // ✅ Safe: checked before return
}

Real-World Application: State Machine

type State = "idle" | "loading" | "success" | "error";

// Before - bug
function handleRequest() {
  let state: State;
  let data: any;

  // ❌ Error: state used before being assigned
  if (state === "loading") {
    // BUG: state is undefined
  }

  // ❌ Error: data used before being assigned
  return data;
}

// After - fixed
function handleRequest() {
  let state: State = "idle"; // ✅ Initialize
  let data: any | undefined;

  state = "loading";

  try {
    data = fetchData();
    state = "success";
  } catch (error) {
    state = "error";
  }

  if (state === "success" && data !== undefined) {
    return data; // ✅ Safe: checked
  }

  throw new Error(`Request failed: ${state}`);
}

Refined JSON Import Attributes

Feature: Better support for JSON module imports with with { type: "json" } attribute syntax.

Improved Type Inference

// Enhanced JSON import type inference
import config from "./config.json" with { type: "json" };
// ✅ Type inferred from JSON structure with better precision

type Config = typeof config;
// ✅ More accurate type inference
// - Preserves literal types
// - Handles nested objects better
// - Maintains readonly properties

// config.json
{
  "app": {
    "name": "MyApp",
    "version": "1.0.0",
    "features": ["auth", "payments"]
  },
  "database": {
    "host": "localhost",
    "port": 5432
  }
}

// ✅ Type automatically inferred as:
type Config = {
  readonly app: {
    readonly name: "MyApp";
    readonly version: "1.0.0";
    readonly features: readonly ["auth", "payments"];
  };
  readonly database: {
    readonly host: "localhost";
    readonly port: 5432;
  };
};

Real-World Application: Configuration Management

// app/config.json
{
  "api": {
    "baseUrl": "https://api.example.com",
    "timeout": 30000,
    "retries": 3
  },
  "features": {
    "analytics": true,
    "darkMode": false
  }
}

// app/settings.ts
import appConfig from "./config.json" with { type: "json" };

// ✅ Type-safe configuration access
export const API_BASE_URL = appConfig.api.baseUrl;
// Type: "https://api.example.com" (literal)

export const API_TIMEOUT = appConfig.api.timeout;
// Type: 30000 (literal number)

export function initializeApp() {
  configureAPI({
    baseUrl: API_BASE_URL,
    timeout: API_TIMEOUT,
    retries: appConfig.api.retries, // ✅ Type: 3
  });

  if (appConfig.features.analytics) {
    // ✅ Type: boolean (true literal)
    enableAnalytics();
  }
}

Real-World Application: Internationalization

// locales/en.json
{
  "common": {
    "welcome": "Welcome",
    "goodbye": "Goodbye"
  },
  "errors": {
    "notFound": "Not found",
    "unauthorized": "Unauthorized"
  }
}

// locales/id.json
{
  "common": {
    "welcome": "Selamat datang",
    "goodbye": "Selamat tinggal"
  },
  "errors": {
    "notFound": "Tidak ditemukan",
    "unauthorized": "Tidak diizinkan"
  }
}

// i18n.ts
import en from "./locales/en.json" with { type: "json" };
import id from "./locales/id.json" with { type: "json" };

type TranslationKey = keyof typeof en;
// Type: "common" | "errors"

type Locale = "en" | "id";

const translations = { en, id } as const;

export function translate(locale: Locale, key: string): string {
  const keys = key.split(".");
  let result: any = translations[locale];

  for (const k of keys) {
    result = result[k];
  }

  return result;
}

// Usage with type safety
const greeting = translate("en", "common.welcome");
// ✅ Returns: "Welcome"

const error = translate("id", "errors.notFound");
// ✅ Returns: "Tidak ditemukan"

Breaking Changes

Minimal breaking changes (stricter error detection):

  1. Never-initialized variables - May reveal existing bugs in conditional initialization
  2. Path rewriting - May change resolution in complex rootDirs setups
  3. JSON import attributes - Stricter type inference may require type adjustments

Migration Guide

Step 1: Update TypeScript

npm install -D typescript@5.7

Step 2: Fix Never-Initialized Variable Errors

Initialize variables before use:

// Before
let value: string;
if (condition) {
  value = "yes";
}
console.log(value); // ❌ Error

// After - initialize
let value: string = "default";
if (condition) {
  value = "yes";
}
console.log(value); // ✅ OK

Step 3: Review Path Rewriting Changes

Check rootDirs imports resolve correctly:

// Verify imports across rootDirs boundaries
import { Types } from "../generated/types";
// Ensure paths work with new rewriting

Step 4: Update JSON Import Syntax

Use with { type: "json" } syntax:

// Before (deprecated)
import config from "./config.json" assert { type: "json" };

// After
import config from "./config.json" with { type: "json" };

Step 5: Leverage Enhanced JSON Types

Use inferred types from JSON imports:

import config from "./config.json" with { type: "json" };

type ConfigType = typeof config;
// ✅ Precise type inference

Performance Improvements

Module Resolution:

  • Faster path rewriting in large monorepos
  • Optimized rootDirs resolution
  • Better caching for JSON imports

Type Checking:

  • Faster never-initialized variable detection
  • Improved control flow analysis
  • Optimized JSON type inference

Summary

TypeScript 5.7 (January 2025) improved monorepo support and variable safety:

  • Path Rewriting for rootDirs - Correct relative paths across rootDir boundaries
  • Never-Initialized Variable Checks - Catch variables used before assignment
  • Refined JSON Import Attributes - Better type inference with with { type: "json" }
  • Monorepo Improvements - Better support for complex monorepo setups

Impact: Path rewriting simplifies monorepo imports, while never-initialized checks prevent common runtime bugs.

Next Steps:

References

Last updated