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.
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.
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:
undefinedused 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!