Learn advanced TypeScript patterns and techniques to write more maintainable, type-safe code. Master type manipulation, generics, decorators, and utility types. 🚀
Type Manipulation 🎯
// Mapped Types
type ReadOnly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
// Conditional Types
type IsString<T> = T extends string ? true : false;
type IsArray<T> = T extends any[] ? true : false;
// Type Inference in Conditional Types
type ElementType<T> = T extends (infer U)[] ? U : never;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Template Literal Types
type EventName<T extends string> = `${T}Changed`;
type PropName<T extends string> = `get${Capitalize<T>}`;
// Examples
interface User {
id: number;
name: string;
email: string;
}
type ReadOnlyUser = ReadOnly<User>;
type OptionalUser = Optional<User>;
type NullableUser = Nullable<User>;
// Usage
const user: ReadOnlyUser = {
id: 1,
name: "John",
email: "john@example.com"
};
// Error: Cannot assign to 'name' because it is a read-only property
// user.name = "Jane";
Advanced Generics 🔄
// Generic Constraints
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(arg: T): number {
return arg.length;
}
// Generic Classes with Multiple Type Parameters
class DataContainer<TData, TError> {
private data: TData | null = null;
private error: TError | null = null;
setData(data: TData) {
this.data = data;
this.error = null;
}
setError(error: TError) {
this.error = error;
this.data = null;
}
getData(): TData | null {
return this.data;
}
getError(): TError | null {
return this.error;
}
}
// Generic Type Guards
function isArray<T>(value: T | T[]): value is T[] {
return Array.isArray(value);
}
// Generic Factory
interface Constructor<T> {
new(...args: any[]): T;
}
function createInstance<T>(
ctor: Constructor<T>,
...args: any[]
): T {
return new ctor(...args);
}
// Generic Mixins
type Constructor2<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor2>(Base: TBase) {
return class extends Base {
timestamp = new Date();
};
}
function Activatable<TBase extends Constructor2>(Base: TBase) {
return class extends Base {
isActive = false;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
};
}
// Usage
class User {
constructor(public name: string) {}
}
const TimestampedUser = Timestamped(User);
const ActivatableUser = Activatable(
Timestamped(User)
);
const user = new ActivatableUser("John");
user.activate();
console.log(user.isActive); // true
console.log(user.timestamp); // Date object
Decorators 🎨
// Method Decorator
function log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
// Property Decorator
function required(
target: any,
propertyKey: string
) {
let value: any;
const getter = function() {
return value;
};
const setter = function(newVal: any) {
if (newVal === undefined || newVal === null) {
throw new Error(
`${propertyKey} is required`
);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
// Class Decorator
function singleton<T extends { new(...args: any[]): {} }>(
constructor: T
) {
let instance: T;
return class extends constructor {
constructor(...args: any[]) {
if (instance) {
return instance;
}
super(...args);
instance = this;
}
};
}
// Parameter Decorator
function validate(
target: any,
propertyKey: string,
parameterIndex: number
) {
const validationMetadataKey = `validation_${propertyKey}_${parameterIndex}`;
if (!Reflect.hasMetadata(validationMetadataKey, target)) {
Reflect.defineMetadata(
validationMetadataKey,
[],
target
);
}
}
// Usage
@singleton
class UserService {
@required
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
@log
async getUser(@validate id: string) {
// Implementation
}
}
Utility Types 🛠️
// Record Type
type PageInfo = {
title: string;
path: string;
};
type Pages = Record<string, PageInfo>;
const pages: Pages = {
home: { title: "Home", path: "/" },
about: { title: "About", path: "/about" }
};
// Partial and Required
interface Config {
host: string;
port: number;
secure: boolean;
}
function updateConfig(
config: Config,
updates: Partial<Config>
): Config {
return { ...config, ...updates };
}
// Pick and Omit
interface User {
id: number;
name: string;
email: string;
password: string;
}
type PublicUser = Omit<User, "password">;
type Credentials = Pick<User, "email" | "password">;
// Extract and Exclude
type StringOrNumber = string | number;
type StringOnly = Extract<StringOrNumber, string>;
type NumberOnly = Exclude<StringOrNumber, string>;
// ReturnType and Parameters
function createUser(name: string, age: number): User {
return { id: 1, name, age };
}
type CreateUserParams = Parameters<typeof createUser>;
type CreateUserReturn = ReturnType<typeof createUser>;
// Readonly and DeepReadonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
// NonNullable and Required
type Config2 = {
debug?: boolean;
api?: string | null;
};
type RequiredConfig = Required<NonNullable<Config2>>;
Advanced Type Patterns 🎯
// Discriminated Unions
type Success<T> = {
type: 'success';
data: T;
};
type Error = {
type: 'error';
error: string;
};
type Result<T> = Success<T> | Error;
function handleResult<T>(result: Result<T>) {
if (result.type === 'success') {
console.log(result.data);
} else {
console.error(result.error);
}
}
// Branded Types
type Brand<K, T> = K & { __brand: T };
type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
// Error: Type 'EUR' is not assignable to type 'USD'
// addUSD(10 as EUR, 20 as USD);
// Type-safe Event Emitter
type EventMap = {
'user:login': { userId: string };
'user:logout': { userId: string };
'error': { message: string };
};
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: Partial<
Record<keyof T, Array<(data: T[keyof T]) => void>>
> = {};
on<K extends keyof T>(
event: K,
callback: (data: T[K]) => void
) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
emit<K extends keyof T>(
event: K,
data: T[K]
) {
this.listeners[event]?.forEach(
callback => callback(data)
);
}
}
// Builder Pattern with Method Chaining
class QueryBuilder<T> {
private query: Partial<T> = {};
where<K extends keyof T>(
key: K,
value: T[K]
): this {
this.query[key] = value;
return this;
}
select<K extends keyof T>(
...keys: K[]
): Pick<T, K> {
const result: Partial<T> = {};
keys.forEach(key => {
if (this.query[key] !== undefined) {
result[key] = this.query[key];
}
});
return result as Pick<T, K>;
}
}
Best Practices 📝
- Use strict type checking
- Leverage type inference
- Use discriminated unions
- Implement proper generics
- Use utility types
- Write type-safe code
- Document complex types
- Use branded types
- Implement proper decorators
- Follow TypeScript best practices
Additional Resources
TypeScript provides powerful tools for type-safe programming. This guide covers advanced patterns and techniques for writing more maintainable and robust TypeScript code.