Tutorial
May 30, 2026
22 min read

TypeScript Advanced Patterns 2026: Master Type-Safe Development

Deep dive into advanced TypeScript patterns including generics, utility types, decorators, conditional types, and type guards. Build robust, type-safe applications with confidence.

Rehman Farouq

Full Stack Developer | TypeScript Expert

Introduction to Advanced TypeScript

TypeScript goes beyond basic type annotations. Advanced patterns enable you to create flexible, reusable, and type-safe code that scales with your application. From generics to decorators, these patterns transform how you write TypeScript.

In this comprehensive guide, we'll explore the most powerful TypeScript patterns that every senior developer should know.

Advanced Generics

Generics allow you to create reusable components that work with different types while maintaining type safety.

// Basic generic function
function identity<T>(arg: T): T {
  return arg;
}

// Generic with constraints
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

// Generic interfaces
interface Box<T> {
  value: T;
  getValue(): T;
}

class GenericBox<T> implements Box<T> {
  constructor(private value: T) {}
  
  getValue(): T {
    return this.value;
  }
}

// Generic with default type
interface ApiResponse<T = any> {
  data: T;
  status: number;
  message: string;
}

// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

// Generic with keyof operator
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

Generic Best Practices

  • Use descriptive type parameter names (T, U, V)
  • Apply constraints when needed with extends
  • Provide default types for flexibility

Utility Types

TypeScript provides built-in utility types that transform types in powerful ways.

// Partial - Make all properties optional
interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = Partial<User>;

// Required - Make all properties required
type RequiredUser = Required<PartialUser>;

// Readonly - Make all properties readonly
type ReadonlyUser = Readonly<User>;

// Pick - Select specific properties
type UserSummary = Pick<User, 'id' | 'name'>;

// Omit - Remove specific properties
type CreateUser = Omit<User, 'id'>;

// Record - Create object type with specific keys
type UserMap = Record<string, User>;

// Exclude - Exclude types from union
type Primitive = Exclude<string | number | boolean, string>;

// Extract - Extract types from union
type StringOrNumber = Extract<string | number | boolean, string | number>;

// NonNullable - Remove null and undefined
type NonNullableUser = NonNullable<User | null>;

// ReturnType - Get return type of function
type UserCreator = () => User;
type UserType = ReturnType<UserCreator>;

// Parameters - Get parameter types of function
type UserParams = Parameters<(id: number, name: string) => void>;

// Awaited - Unwrap Promise type
type AsyncUser = Promise<User>;
type ResolvedUser = Awaited<AsyncUser>;

Conditional Types

Conditional types select one type over another based on a condition.

// Basic conditional type
type IsArray<T> = T extends any[] ? true : false;

type Test1 = IsArray<string[]>; // true
type Test2 = IsArray<string>;  // false

// Conditional with union types
type ToArray<T> = T extends any[] ? T : T[];

type Result1 = ToArray<string>;   // string[]
type Result2 = ToArray<number[]>; // number[]

// Distributive conditional types
type Flatten<T> = T extends any[] ? T[number] : T;

type Flat1 = Flatten<string[]>;      // string
type Flat2 = Flatten<number>;        // number
type Flat3 = Flatten<(string | number)[]>; // string | number

// Infer keyword
type UnboxArray<T> = T extends (infer U)[] ? U : T;

type Unboxed1 = UnboxArray<string[]>; // string
type Unboxed2 = UnboxArray<number>;   // number

// Conditional type with constraints
type NonNullable<T> = T extends null | undefined ? never : T;

// Complex conditional types
type TypeName<T> = 
  T extends string ? 'string' :
  T extends number ? 'number' :
  T extends boolean ? 'boolean' :
  T extends undefined ? 'undefined' :
  T extends Function ? 'function' :
  'object';

type T1 = TypeName<string>;   // 'string'
type T2 = TypeName<number[]>; // 'object'

Type Guards & Discriminated Unions

Type guards narrow down types within conditional blocks.

// typeof type guard
function process(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase(); // string
  }
  return value.toFixed(2); // number
}

// instanceof type guard
class Dog {
  bark() { console.log('Woof!'); }
}

class Cat {
  meow() { console.log('Meow!'); }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

// Custom type guard
interface Bird {
  fly(): void;
}

interface Fish {
  swim(): void;
}

function isBird(animal: Bird | Fish): animal is Bird {
  return 'fly' in animal;
}

function move(animal: Bird | Fish) {
  if (isBird(animal)) {
    animal.fly();
  } else {
    animal.swim();
  }
}

// Discriminated unions
interface Circle {
  kind: 'circle';
  radius: number;
}

interface Square {
  kind: 'square';
  side: number;
}

type Shape = Circle | Square;

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'square':
      return shape.side ** 2;
  }
}

// in operator type guard
interface HasEmail {
  email: string;
}

interface HasPhone {
  phone: string;
}

function contact(user: HasEmail | HasPhone) {
  if ('email' in user) {
    console.log(user.email);
  } else {
    console.log(user.phone);
  }
}

Mapped Types

Mapped types transform properties of an existing type.

// Basic mapped type
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type ReadonlyUser = Readonly<User>;

// Make optional
type Optional<T> = {
  [P in keyof T]?: T[P];
};

// Nullable
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

// Template literal types
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

// Key remapping
type Stringify<T> = {
  [P in keyof T as string & P]: string;
};

// Conditional mapped types
type PickByType<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P];
};

interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

type StringProperties = PickByType<Product, string>;
// { name: string; }

Decorators

Decorators provide a way to add annotations and meta-programming syntax for class declarations and members.

// Enable decorators in tsconfig.json
// "experimentalDecorators": true

// Class decorator
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class MyClass {
  // ...
}

// 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;
}

class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
}

// Property decorator
function format(target: any, propertyKey: string) {
  let value: string;
  
  const getter = () => value;
  const setter = (newValue: string) => {
    value = newValue.trim().toUpperCase();
  };
  
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

class User {
  @format
  name: string = '';
}

// Parameter decorator
function required(target: any, propertyKey: string, parameterIndex: number) {
  const existingRequiredParameters = Reflect.getMetadata(
    'required',
    target,
    propertyKey
  ) || [];
  
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(
    'required',
    existingRequiredParameters,
    target,
    propertyKey
  );
}

class Validator {
  validate(@required value: string) {
    console.log(value);
  }
}

Template Literal Types

Template literal types allow you to manipulate string types at the type level.

// Basic template literal types
type Greeting = "Hello" | "Hi";
type Name = "World" | "User";
type Message = `${Greeting}, ${Name}!`;
// "Hello, World!" | "Hello, User!" | "Hi, World!" | "Hi, User!"

// String manipulation
type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<'click'>; // "onClick"
type ChangeEvent = EventName<'change'>; // "onChange"

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<'hello'>; // "HELLO"
type Lower = Lowercase<'HELLO'>; // "hello"
type Cap = Capitalize<'hello'>; // "Hello"
type Uncap = Uncapitalize<'Hello'>; // "hello"

// Complex template literal types
type CssColor = 'red' | 'blue' | 'green';
type CssProperty = 'background' | 'color' | 'border';
type CssRule = `${CssProperty}: ${CssColor}`;

// String union manipulation
type AllKeys<T> = T extends any ? keyof T : never;

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface State {
  count: number;
  name: string;
}

type StateGetters = Getters<State>;
// { getCount: () => number; getName: () => string; }

TypeScript Best Practices

Type Safety

  • • Enable strict mode in tsconfig
  • • Avoid any, use unknown instead
  • • Use type assertions sparingly
  • • Prefer interfaces over type aliases for objects

Code Organization

  • • Keep types close to usage
  • • Use barrel files for exports
  • • Separate utility types
  • • Document complex types

Performance

  • • Avoid excessive generics
  • • Use type inference when possible
  • • Don't over-engineer types
  • • Profile type checking performance

Maintainability

  • • Use meaningful type names
  • • Leverage utility types
  • • Create reusable type utilities
  • • Keep types simple and readable

Conclusion

Advanced TypeScript patterns empower you to write more robust, maintainable, and type-safe code. By mastering generics, utility types, conditional types, and other advanced features, you can create sophisticated type systems that catch errors at compile time.

Remember that TypeScript is a tool to help you write better JavaScript. Use these patterns judiciously and always prioritize code readability and maintainability.

Ready to Master TypeScript?

Explore more TypeScript tutorials and build type-safe applications!