Advanced TypeScript: A Deep Dive into Modern TypeScript Development
Introduction
TypeScript has become the go-to language for building scalable JavaScript applications. In this comprehensive guide, we'll explore advanced TypeScript concepts that will enhance your development skills and help you write more type-safe code.
1. Advanced Type System Features
Conditional Types
Understanding complex type relationships:
type IsArray<T> = T extends any[] ? true : false;
type IsString<T> = T extends string ? true : false;
// Usage
type CheckArray = IsArray<string[]>; // true
type CheckString = IsString<"hello">; // true
// More complex conditional types
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type ArrayElement<T> = T extends (infer U)[] ? U : never;
// Example usage
type PromiseString = UnwrapPromise<Promise<string>>; // string
type NumberArray = ArrayElement<number[]>; // number
Template Literal Types
Leveraging string literal types for better type safety:
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIEndpoint = '/users' | '/posts' | '/comments';
type APIRoute = `${HTTPMethod} ${APIEndpoint}`;
// Valid routes
const validRoute: APIRoute = 'GET /users';
const validRoute2: APIRoute = 'POST /posts';
// Error: Type '"PATCH /users"' is not assignable to type 'APIRoute'
// const invalidRoute: APIRoute = 'PATCH /users';
// Dynamic template literal types
type PropEventType<T extends string> = `on${Capitalize<T>}`;
type ButtonEvents = PropEventType<'click' | 'hover' | 'focus'>;
// Results in: 'onClick' | 'onHover' | 'onFocus'
2. Advanced Generics
Generic Constraints and Defaults
Creating flexible yet type-safe generic interfaces:
interface Database<T extends { id: string }> {
find(id: string): Promise<T | null>;
create(data: Omit<T, 'id'>): Promise<T>;
update(id: string, data: Partial<T>): Promise<T>;
delete(id: string): Promise<boolean>;
}
// Implementation example
class MongoDatabase<T extends { id: string }> implements Database<T> {
constructor(private collection: string) {}
async find(id: string): Promise<T | null> {
// Implementation
return null;
}
async create(data: Omit<T, 'id'>): Promise<T> {
// Implementation
return { id: 'generated', ...data } as T;
}
async update(id: string, data: Partial<T>): Promise<T> {
// Implementation
return { id, ...data } as T;
}
async delete(id: string): Promise<boolean> {
// Implementation
return true;
}
}
Mapped Types and Key Remapping
Advanced type transformations:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// Results in:
// {
// getName: () => string;
// getAge: () => number;
// }
// Advanced key remapping with filtering
type FilteredKeys<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
};
interface Mixed {
name: string;
count: number;
isActive: boolean;
data: object;
}
type StringKeys = FilteredKeys<Mixed, string>;
// Results in: { name: string }
3. Advanced Decorators
Custom Property Decorators
Creating powerful metadata-driven decorators:
function ValidateProperty(validationFn: (value: any) => boolean) {
return function (target: any, propertyKey: string) {
let value: any;
const getter = function() {
return value;
};
const setter = function(newVal: any) {
if (!validationFn(newVal)) {
throw new Error(`Invalid value for ${propertyKey}`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
}
class User {
@ValidateProperty((value) => typeof value === 'string' && value.length > 0)
name: string;
@ValidateProperty((value) => typeof value === 'number' && value >= 0)
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
4. Advanced Utility Types
Custom Utility Types
Building powerful type transformations:
// Deep Partial type
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
// Deep Required type
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object
? DeepRequired<T[P]>
: T[P];
};
// Deep Readonly type
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
// Example usage
interface Config {
server: {
port: number;
host: string;
options: {
timeout: number;
retries: number;
};
};
database: {
url: string;
name: string;
};
}
type PartialConfig = DeepPartial<Config>;
// Now we can have partial nested objects
const config: PartialConfig = {
server: {
port: 3000
// host and options can be omitted
}
};
5. Type-Safe API Patterns
Builder Pattern with Type Safety
Implementing the builder pattern with full type safety:
class RequestBuilder<T = {}> {
private data: T;
constructor(data: T = {} as T) {
this.data = data;
}
with<K extends string, V>(
key: K,
value: V
): RequestBuilder<T & { [key in K]: V }> {
return new RequestBuilder({
...this.data,
[key]: value,
});
}
build(): T {
return this.data;
}
}
// Usage
const request = new RequestBuilder()
.with('url', 'https://api.example.com')
.with('method', 'GET')
.with('headers', { 'Content-Type': 'application/json' })
.build();
// Type is inferred correctly
type Request = typeof request;
// {
// url: string;
// method: string;
// headers: { 'Content-Type': string };
// }
6. Advanced Error Handling
Type-Safe Error Handling
Creating a robust error handling system:
class Result<T, E extends Error> {
private constructor(
private value: T | null,
private error: E | null
) {}
static ok<T>(value: T): Result<T, never> {
return new Result(value, null);
}
static err<E extends Error>(error: E): Result<never, E> {
return new Result(null, error);
}
map<U>(fn: (value: T) => U): Result<U, E> {
if (this.value === null) {
return new Result(null, this.error);
}
return new Result(fn(this.value), null);
}
mapError<F extends Error>(fn: (error: E) => F): Result<T, F> {
if (this.error === null) {
return new Result(this.value, null);
}
return new Result(null, fn(this.error));
}
unwrap(): T {
if (this.value === null) {
throw this.error;
}
return this.value;
}
}
// Usage example
function divide(a: number, b: number): Result<number, Error> {
if (b === 0) {
return Result.err(new Error('Division by zero'));
}
return Result.ok(a / b);
}
const result = divide(10, 2)
.map(n => n * 2)
.unwrap(); // 10
Conclusion
These advanced TypeScript patterns demonstrate the language's power in creating type-safe and maintainable applications. By mastering these concepts, you'll be better equipped to build robust applications that leverage TypeScript's type system to its fullest potential.
Additional Resources
Share your experiences with these patterns in the comments below! What advanced TypeScript features have you found most useful in your projects?
Tags: #typescript #javascript #webdevelopment #programming #typing