Introduction
TypeScript continues to evolve, bringing powerful new features that improve developer productivity and code safety. With TypeScript 5.5, the language introduces significant improvements to type inference, control flow analysis, and declaration emit. This guide explores the latest features and best practices for modern TypeScript development.
What’s New in TypeScript 5.5
Key Features
- Inferred Type Predicates: Automatically infer
istype predicates from functions - Isolated Declarations: Enable stricter declaration file generation
- Control Flow Narrowing for Constants: Better type narrowing for const variables
- JSDoc
@snippetSupport: Improved documentation
Inferred Type Predicates
The Feature
TypeScript can now automatically infer when a function returns a type predicate:
// TypeScript 5.5+ automatically infers this as a type predicate
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function process(value: unknown) {
if (isString(value)) {
// value is narrowed to string here
console.log(value.toUpperCase());
}
}
// Works with array methods too
const numbers = [1, 2, 'three', 4, 'five'];
const strings = numbers.filter(isString); // TypeScript knows this is (string | number)[]
Practical Example
interface Animal {
name: string;
speak(): void;
}
interface Dog extends Animal {
breed: string;
}
interface Cat extends Animal {
isIndoor: boolean;
}
function isDog(animal: Animal): animal is Dog {
return 'breed' in animal;
}
function isCat(animal: Animal): animal is Cat {
return 'isIndoor' in animal;
}
function processAnimals(animals: Animal[]) {
const dogs = animals.filter(isDog);
const cats = animals.filter(isCat);
dogs.forEach(dog => {
console.log(dog.breed); // TypeScript knows this is Dog
});
cats.forEach(cat => {
console.log(cat.isIndoor); // TypeScript knows this is Cat
});
}
Isolated Declarations
The Feature
Isolated declarations allow faster builds and stricter type checking:
// tsconfig.json
{
"compilerOptions": {
"isolatedDeclarations": true
}
}
With Declaration Files
// With isolatedDeclarations, TypeScript requires explicit type annotations
// for exported functions that will be emitted as .d.ts
// โ
Valid with isolatedDeclarations
export function add(a: number, b: number): number {
return a + b;
}
// โ Invalid - TypeScript can't infer return type for declaration
export const multiply = (a: number, b: number) => a * b;
// โ
Must explicitly annotate
export const multiply: (a: number, b: number) => number = (a, b) => a * b;
// Works with classes too
export class Calculator {
// Must have explicit return types
add(a: number, b: number): number {
return a + b;
}
}
When to Use
// Good for: Large projects, library authors, monorepos
// Benefit: Faster build times, clearer type contracts
// Not ideal for: Rapid prototyping, small projects
// Drawback: Requires more explicit type annotations
Control Flow Narrowing
Narrowing for const
// TypeScript 5.5+ improves narrowing for const variables
declare const config: {
mode: 'development' | 'production' | 'staging';
port: number;
};
function processConfig() {
// TypeScript now properly narrows const values
if (config.mode === 'development') {
// config.mode is narrowed to 'development' (literal type)
console.log('Dev mode, port:', config.port);
}
switch (config.mode) {
case 'development':
enableDebugMode();
break;
case 'production':
enableProductionMode();
break;
}
}
function enableDebugMode() {}
function enableProductionMode() {}
Template Literal Types
type EventName = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<EventName>}`;
// TypeScript can now better infer these types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type HandlerMap = {
[K in HTTPMethod as `${Lowercase<K>}Handler`]: () => void;
};
Best Practices
1. Use Strict Mode
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noUncheckedIndexedAccess": true
}
}
2. Prefer Type Inference
// โ
Good - let TypeScript infer
const numbers = [1, 2, 3];
const person = { name: 'John', age: 30 };
// โ
Good - explicit when needed for clarity
const MAX_RETRIES = 3 as const;
interface Config {
apiUrl: string;
}
3. Use Type Guards
// Custom type guard
function hasProperty<T>(obj: unknown, key: string): obj is T {
return typeof obj === 'object' && obj !== null && key in obj;
}
// Usage
function process(data: unknown) {
if (hasProperty<{ message: string }>(data, 'message')) {
console.log(data.message); // Typed as { message: string }
}
}
4. Leverage Utility Types
// Partial - make all properties optional
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// Omit - exclude properties
type UserWithoutEmail = Omit<User, 'email'>;
// Record - create object types
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
Performance Tips
1. Incremental Compilation
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo"
}
}
2. Project References
// tsconfig.json (root)
{
"references": [
{ "path": "./packages/common" },
{ "path": "./packages/utils" },
{ "path": "./packages/app" }
]
}
3. Skip Lib Check
{
"compilerOptions": {
"skipLibCheck": true
}
}
External Resources
Official
Tools
Key Takeaways
- Inferred type predicates simplify type guards
- Isolated declarations improve build performance
- Control flow narrowing works better with const
- Best practices: Use strict mode, leverage inference, use type guards
- Performance: Incremental builds, project references
Comments