Advanced TypeScript Features
/ 8 min read
Introduction
TypeScript offers a rich set of advanced features that enable developers to build type-safe, maintainable, and scalable applications. This guide explores these advanced features, providing practical examples and real-world use cases.
Type System Features
TypeScript’s type system includes powerful features that go beyond basic type checking, allowing for complex type manipulations and guarantees.
1. Conditional Types
Conditional types enable you to create types that depend on other types, similar to if statements in regular programming.
// Basic conditional typetype IsString<T> = T extends string ? true : false;type Result1 = IsString<string>; // truetype Result2 = IsString<number>; // false
// Distributive conditional typestype ToArray<T> = T extends any ? T[] : never;type StrNumArr = ToArray<string | number>; // string[] | number[]
// Real-world example: API response typetype ApiResponse<T> = T extends void ? { success: true } : { success: true; data: T } | { success: false; error: string };
// Usageinterface User { id: number; name: string;}
async function fetchUser(id: number): Promise<ApiResponse<User>> { try { const response = await fetch(`/api/users/${id}`); const user = await response.json(); return { success: true, data: user }; } catch (error) { return { success: false, error: error.message }; }}2. Mapped Types with Key Remapping
Mapped types allow you to transform the properties of an existing type into a new type.
// Advanced mapped type with key remappingtype Events<T> = { [K in keyof T as `${string & K}Changed`]: (newValue: T[K]) => void;} & { [K in keyof T as `${string & K}Validated`]: (value: T[K]) => boolean;};
interface FormData { username: string; email: string; age: number;}
type FormEvents = Events<FormData>;/* Result:{ usernameChanged: (newValue: string) => void; emailChanged: (newValue: string) => void; ageChanged: (newValue: number) => void; usernameValidated: (value: string) => boolean; emailValidated: (value: string) => boolean; ageValidated: (value: number) => boolean;}*/
// Practical example: Form validationclass Form implements FormEvents { private data: FormData = { username: '', email: '', age: 0 };
usernameChanged(newValue: string) { if (this.usernameValidated(newValue)) { this.data.username = newValue; } }
usernameValidated(value: string) { return value.length >= 3; }
emailChanged(newValue: string) { if (this.emailValidated(newValue)) { this.data.email = newValue; } }
emailValidated(value: string) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); }
ageChanged(newValue: number) { if (this.ageValidated(newValue)) { this.data.age = newValue; } }
ageValidated(value: number) { return value >= 18 && value <= 100; }}3. Template Literal Types
Template literal types combine literal types through template literal strings, enabling powerful string manipulation at the type level.
// Advanced template literal typestype CSSProperties = 'margin' | 'padding' | 'border';type CSSDirections = 'Top' | 'Right' | 'Bottom' | 'Left';type CSSValues = string | number;
type CSSRule = { [P in CSSProperties]: CSSValues;} & { [P in CSSProperties as `${P}${CSSDirections}`]: CSSValues;};
// Usageconst styles: CSSRule = { margin: '10px', marginTop: '20px', marginRight: 15, padding: '5px', paddingBottom: '10px', border: '1px solid black', borderLeft: '2px dashed red'};
// Event handling with template literalstype EventType = 'click' | 'focus' | 'blur' | 'input';type HTMLElements = 'Button' | 'Input' | 'Form';type EventHandler<T extends string> = `on${Capitalize<T>}`;type ElementEvent = `${HTMLElements}${EventType}`;
type ComponentProps = { [E in ElementEvent as EventHandler<E>]?: (event: Event) => void;};
// Usageconst component: ComponentProps = { onButtonClick: (e) => console.log('Button clicked'), onInputFocus: (e) => console.log('Input focused'), onFormSubmit: (e) => console.log('Form submitted')};Advanced Type Manipulation
1. Type Recursion and Deep Modifications
// Advanced recursive type transformationstype DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? T[P] extends Function ? T[P] : DeepReadonly<T[P]> : T[P];};
type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? T[P] extends Function ? T[P] : DeepPartial<T[P]> : T[P];};
type DeepRequired<T> = { [P in keyof T]-?: T[P] extends object ? T[P] extends Function ? T[P] : DeepRequired<T[P]> : T[P];};
// Practical example: Configuration systeminterface Config { api: { endpoint: string; timeout: number; retries: { count: number; delay: number; }; }; features: { darkMode: boolean; notifications: { email: boolean; push: boolean; frequency: { value: number; unit: 'minutes' | 'hours' | 'days'; }; }; };}
type ReadonlyConfig = DeepReadonly<Config>;type PartialConfig = DeepPartial<Config>;
// Usagefunction updateConfig(config: PartialConfig) { // Safe partial updates}
const defaultConfig: ReadonlyConfig = { api: { endpoint: 'https://api.example.com', timeout: 5000, retries: { count: 3, delay: 1000 } }, features: { darkMode: false, notifications: { email: true, push: true, frequency: { value: 1, unit: 'hours' } } }};2. Advanced Type Inference
// Complex type inference patternstype UnwrapPromiseDeep<T> = T extends Promise<infer U> ? UnwrapPromiseDeep<U> : T extends Array<infer V> ? Array<UnwrapPromiseDeep<V>> : T extends object ? { [K in keyof T]: UnwrapPromiseDeep<T[K]> } : T;
// Practical example: API response handlinginterface ApiEndpoints { user: Promise<{ id: number; profile: Promise<{ name: string; preferences: Promise<{ theme: string; notifications: boolean; }>; }>; }>;}
type UnwrappedApi = UnwrapPromiseDeep<ApiEndpoints>;/* Result:{ user: { id: number; profile: { name: string; preferences: { theme: string; notifications: boolean; }; }; };}*/
// Function composition with inferencetype Pipeline<T> = { then<U>(fn: (value: T) => U): Pipeline<U>; value(): T;};
function createPipeline<T>(initial: T): Pipeline<T> { return { then: function<U>(fn: (value: T) => U): Pipeline<U> { return createPipeline(fn(initial)); }, value: () => initial };}
// Usageconst result = createPipeline("hello") .then(str => str.toUpperCase()) .then(str => str.length) .then(num => num * 2) .value(); // Type is number3. Type-Safe Event System
// Advanced event system with type safetytype EventMap = { userLoggedIn: { userId: string; timestamp: number }; userLoggedOut: { userId: string; timestamp: number }; error: { code: number; message: string }; stateChanged: { previous: Record<string, unknown>; current: Record<string, unknown>; };};
class TypeSafeEventEmitter<Events extends Record<string, any>> { private listeners = new Map< keyof Events, Set<(data: Events[keyof Events]) => void> >();
on<E extends keyof Events>( event: E, callback: (data: Events[E]) => void ): () => void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); }
const callbacks = this.listeners.get(event)!; callbacks.add(callback);
// Return unsubscribe function return () => { callbacks.delete(callback); if (callbacks.size === 0) { this.listeners.delete(event); } }; }
emit<E extends keyof Events>(event: E, data: Events[E]): void { this.listeners.get(event)?.forEach(callback => { try { callback(data); } catch (error) { console.error(`Error in ${String(event)} event handler:`, error); } }); }
once<E extends keyof Events>( event: E, callback: (data: Events[E]) => void ): () => void { const unsubscribe = this.on(event, (data) => { unsubscribe(); callback(data); }); return unsubscribe; }
removeAllListeners<E extends keyof Events>(event?: E): void { if (event) { this.listeners.delete(event); } else { this.listeners.clear(); } }}
// Usageconst events = new TypeSafeEventEmitter<EventMap>();
const unsubscribe = events.on('userLoggedIn', ({ userId, timestamp }) => { console.log(`User ${userId} logged in at ${new Date(timestamp)}`);});
events.emit('userLoggedIn', { userId: '123', timestamp: Date.now()});
// Error: Type '"invalid"' is not assignable to parameter of type 'keyof EventMap'// events.emit('invalid', {});Performance Optimizations
1. Type Caching and Computation
// Type computation optimizationtype ComputeDeep<T> = T extends Function ? T : T extends Array<infer U> ? Array<ComputeDeep<U>> : T extends object ? { [K in keyof T]: ComputeDeep<T[K]> } : T;
// Type caching for expensive operationsinterface TypeCache<T> { readonly _brand: unique symbol; value: T;}
function createTypeCache<T>(value: T): TypeCache<T> { return { _brand: Symbol() as any, value };}
// Usage with heavy computationstype HeavyComputation<T> = ComputeDeep<DeepPartial<DeepReadonly<T>>>;
const cachedType = createTypeCache<HeavyComputation<Config>>({} as any);type CachedResult = typeof cachedType.value;2. Lazy Type Evaluation
// Lazy type evaluation patternsinterface LazyType<T> { (): T; _type?: T;}
function createLazyType<T>(factory: () => T): LazyType<T> { const lazy = () => { if (!lazy._type) { lazy._type = factory(); } return lazy._type; }; return lazy as LazyType<T>;}
// Usageconst lazyConfig = createLazyType(() => ({ // Complex configuration object api: { endpoint: 'https://api.example.com', timeout: 5000 }, features: { darkMode: true, notifications: true }}));
type LazyConfig = ReturnType<typeof lazyConfig>;Advanced Configuration and Project Structure
1. Module Resolution and Path Mapping
// tsconfig.json{ "compilerOptions": { "baseUrl": ".", "paths": { "@core/*": ["src/core/*"], "@features/*": ["src/features/*"], "@shared/*": ["src/shared/*"], "@utils/*": ["src/utils/*"] }, "moduleResolution": "node16", "types": ["node", "jest"], "typeRoots": [ "./node_modules/@types", "./src/types" ] }}
// Usage in codeimport { ApiClient } from '@core/api';import { UserService } from '@features/user';import { Logger } from '@utils/logger';2. Project References and Build Optimization
// Root tsconfig.json{ "references": [ { "path": "./packages/shared" }, { "path": "./packages/server" }, { "path": "./packages/client" } ], "files": []}
// Shared package tsconfig.json{ "compilerOptions": { "composite": true, "declaration": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"]}Best Practices and Guidelines
-
Type Safety
- Use strict mode (
"strict": true) - Avoid type assertions (
as) - Leverage type inference
- Use branded types for type safety
- Use strict mode (
-
Code Organization
- Keep related types together
- Use barrel files for exports
- Maintain clear type hierarchies
- Document complex type patterns
-
Performance
- Cache complex type computations
- Use type aliases for reusability
- Consider build time impact
- Optimize module resolution
-
Error Handling
- Use discriminated unions for errors
- Implement proper error boundaries
- Maintain type safety in error handlers
- Document error scenarios
-
Testing
- Write tests for type definitions
- Test edge cases
- Verify type constraints
- Use type assertion tests
Common Pitfalls and Solutions
-
Type System Limitations
- Understanding variance
- Handling circular types
- Managing type inference
- Dealing with type widening
-
Performance Issues
- Over-complex type definitions
- Excessive type recursion
- Large union types
- Type inference bottlenecks
-
Maintainability Challenges
- Type duplication
- Poor type documentation
- Complex type hierarchies
- Unclear type boundaries
Conclusion
Advanced TypeScript features provide powerful tools for building robust, type-safe applications. By understanding and properly implementing these features, you can:
- Create more maintainable and scalable code
- Catch errors at compile-time
- Improve developer experience
- Build better tooling and abstractions
Remember to balance type safety with code complexity, and always choose the simplest solution that meets your requirements.
Series Navigation
- Previous: TypeScript Decorators
- Next: TypeScript with Frameworks