← Back to Articles

TypeScript 5.5: Advanced Type System Features and Real-World Applications

Dive deep into TypeScript 5.5's new features including improved type inference, performance optimizations, and practical patterns for building type-safe applications.

By Urban M.•
TypeScriptJavaScriptWeb DevelopmentProgramming
TypeScript 5.5: Advanced Type System Features and Real-World Applications

TypeScript 5.5: Advanced Type System Features

TypeScript 5.5 brings powerful new features that make building type-safe applications easier and more intuitive. Let's explore the most impactful changes and how to use them in production code.

TypeScript Logo


New Features Overview

1. Inferred Type Predicates

TypeScript can now work out on its own that a function narrows a type. The most visible effect is on filter():

const values = [1, undefined, 3, undefined];
const numbers = values.filter(v => v !== undefined);

// TypeScript 5.4: (number | undefined)[]
// TypeScript 5.5: number[]

Before 5.5 you had to write the predicate yourself, filter((v): v is number => v !== undefined), to get number[]. Now the compiler infers it from the function body.

2. Narrowing for Constant Indexed Accesses

When both the object and the key don't change, TypeScript now remembers checks on obj[key]:

function f1(obj: Record<string, unknown>, key: string) {
  if (typeof obj[key] === "string") {
    obj[key].toUpperCase(); // OK in 5.5; an error in earlier versions
  }
}

3. Regular Expression Syntax Checking

Regular expression literals are now checked for syntax errors at compile time instead of failing at runtime:

let myRegex = /@robot(\s+(please|immediately)))? do some task/;
//                                            ~
// error: Unexpected ')'. Did you mean to escape it with backslash?

4. Isolated Declarations

The new isolatedDeclarations option requires exported functions and values to have explicit type annotations wherever the type can't be read directly from the code. In return, other tools can generate .d.ts declaration files for each file independently, without running the full type checker. This matters most in large monorepos, where declaration generation is often the slowest part of the build.

5. The ${configDir} Variable in tsconfig

Shared base configurations can now refer to the directory of the project that uses them, rather than the directory of the base file:

{
  "compilerOptions": {
    "outDir": "${configDir}/dist",
    "typeRoots": ["${configDir}/node_modules/@types"]
  }
}

Other additions include the JSDoc @import tag and type support for the new ECMAScript Set methods. The TypeScript 5.5 announcement lists everything.


Performance Improvements

Compilation Speed

TypeScript 5.5 includes a number of compiler performance and size optimizations. The TypeScript 5.5 announcement describes them in detail. How much faster your builds get depends on your project, so measure before and after upgrading.


Real-World Patterns

Pattern 1: Type-Safe Event Emitters

interface EventMap {
  'user:login': { userId: string; timestamp: number };
  'user:logout': { userId: string };
  'data:update': { id: string; data: unknown };
}

class TypedEventEmitter<T extends Record<string, unknown>> {
  private listeners = new Map<keyof T, Set<(data: any) => void>>();

  on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(handler);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    const handlers = this.listeners.get(event);
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
  }
}

// Usage
const emitter = new TypedEventEmitter<EventMap>();

emitter.on('user:login', (data) => {
  // data is typed as { userId: string; timestamp: number }
  console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});

emitter.emit('user:login', { 
  userId: '123', 
  timestamp: Date.now() 
}); // ✅ Type-safe

// emitter.emit('user:login', { invalid: true }); // ❌ Type error!

Pattern 2: Builder Pattern with Type Safety

interface QueryBuilder<T> {
  data: T;
  where<K extends keyof T>(field: K, value: T[K]): QueryBuilder<T>;
  select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>>;
  execute(): Promise<T>;
}

interface User {
  id: string;
  name: string;
  email: string;
  age: number;
  active: boolean;
}

// Usage
const query = createQueryBuilder<User>()
  .where('active', true)
  .where('age', 25)
  .select('id', 'name', 'email');

// Result type is automatically: Pick<User, 'id' | 'name' | 'email'>
const result = await query.execute();

Advanced Type Manipulation

Recursive Conditional Types

// Deeply pick nested properties
type DeepPick<T, Path extends string> = 
  Path extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T
      ? { [K in Key]: DeepPick<T[K], Rest> }
      : never
    : Path extends keyof T
      ? Pick<T, Path>
      : never;

interface Database {
  users: {
    profile: {
      name: string;
      avatar: string;
    };
    settings: {
      theme: 'light' | 'dark';
      notifications: boolean;
    };
  };
}

// Usage
type UserName = DeepPick<Database, 'users.profile.name'>;
// Result: { users: { profile: { name: string } } }

Migration Tips

Updating Your Project

# Install TypeScript 5.5
npm install -D typescript@5.5

# Update tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2023"],
    "module": "ESNext",
    "strict": true
  }
}

# Run type checking
npx tsc --noEmit

Common Migration Issues

Issue: Type errors in third-party libraries

Solution: Update type definitions

npm update @types/*

Issue: Options deprecated in TypeScript 5.0 stop working

TypeScript 5.0 deprecated several compiler options, such as importsNotUsedAsValues, preserveValueImports and target: "ES3". In 5.5 they no longer work. Solution: remove them from tsconfig.json; for the two import options, use verbatimModuleSyntax instead.

Issue: undefined used as a type name

Declaring a type alias called undefined is now an error. Solution: rename it.


Best Practices

✅ DO: Use Const Type Parameters (since TypeScript 5.0)

A const modifier on a type parameter makes TypeScript infer the most specific type, as if the caller had written as const:

function routes<const T extends readonly string[]>(paths: T): T {
  return paths;
}

const r = routes(['/users', '/posts']);
// Type: readonly ["/users", "/posts"] instead of string[]

✅ DO: Leverage Type Inference

// Let TypeScript infer the return type
function createUser(name: string, age: number) {
  return { name, age, created: Date.now() };
}

❌ DON'T: Overuse any

// Bad
function process(data: any) { ... }

// Good
function process<T>(data: T) { ... }

✅ DO: Use Discriminated Unions

type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

function handleResult<T>(result: Result<T>) {
  if (result.success) {
    // TypeScript knows result.data exists here
    console.log(result.data);
  } else {
    // TypeScript knows result.error exists here
    console.error(result.error);
  }
}

Conclusion

TypeScript 5.5 represents a significant step forward in type safety and developer productivity. The new features, combined with performance improvements, make it an essential upgrade for any TypeScript project.

Key Benefits

✨ Smarter inference with inferred type predicates and indexed-access narrowing
🔍 Compile-time regex checking
📦 Faster declaration builds with isolated declarations
🚀 Compiler performance optimizations

Official Resources


Need help migrating to TypeScript 5.5? Our team specializes in TypeScript consulting and can help optimize your codebase. Get in touch today!