Back to Blog

Advanced TypeScript: Mastering Type System for Better Code

Frontend Team
February 14, 2025
9 min read
Web Development

Advanced TypeScript: Mastering Type System for Better Code

TypeScript’s type system is incredibly powerful. Let’s explore advanced features that will take your TypeScript skills to the next level.

Generics

Basic Generics

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

const num = identity<number>(42);
const str = identity<string>("hello");

Generic Constraints

interface Lengthwise {
  length: number;
}

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

logLength("hello");        // ✅
logLength([1, 2, 3]);      // ✅
logLength({ length: 10 }); // ✅
// logLength(123);         // ❌ Error

Multiple Type Parameters

function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const result = merge(
  { name: "John" },
  { age: 30 }
);
// result: { name: string; age: number }

Conditional Types

Basic Conditional Types

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

Infer Keyword

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { name: "John", age: 30 };
}

type User = ReturnType<typeof getUser>;
// { name: string; age: number }

Distributive Conditional Types

type ToArray<T> = T extends any ? T[] : never;

type StrOrNumArray = ToArray<string | number>;
// string[] | number[]

Mapped Types

Basic Mapped Types

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

type Partial<T> = {
  [P in keyof T]?: T[P];
};

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

type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;

Template Literal Types

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

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

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

Utility Types

Built-in Utility Types

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

// Omit - Exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;

// Record - Create object type
type PageInfo = Record<'home' | 'about' | 'contact', { title: string }>;

// Exclude - Exclude from union
type T1 = Exclude<'a' | 'b' | 'c', 'a'>;  // 'b' | 'c'

// Extract - Extract from union
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'f'>;  // 'a'

// NonNullable - Remove null and undefined
type T3 = NonNullable<string | null | undefined>;  // string

Custom Utility Types

// Deep Partial
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Required Recursive
type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};

// Mutable (remove readonly)
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

Type Guards

User-Defined Type Guards

interface Fish {
  swim: () => void;
}

interface Bird {
  fly: () => void;
}

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim();  // TypeScript knows it's Fish
  } else {
    pet.fly();   // TypeScript knows it's Bird
  }
}

Assertion Functions

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Not a string!');
  }
}

function processValue(value: unknown) {
  assertIsString(value);
  // TypeScript now knows value is string
  console.log(value.toUpperCase());
}

Advanced Patterns

Discriminated Unions

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; size: number }
  | { kind: 'rectangle'; width: number; height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'square':
      return shape.size ** 2;
    case 'rectangle':
      return shape.width * shape.height;
  }
}

Branded Types

type Brand<K, T> = K & { __brand: T };

type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;

function usd(amount: number): USD {
  return amount as USD;
}

function eur(amount: number): EUR {
  return amount as EUR;
}

const dollars = usd(100);
const euros = eur(100);

// const sum = dollars + euros;  // ❌ Type error!

Builder Pattern with Types

class RequestBuilder<T extends { url?: string; method?: string; body?: any }> {
  private config: T;

  constructor(config: T) {
    this.config = config;
  }

  url<U extends string>(url: U): RequestBuilder<T & { url: U }> {
    return new RequestBuilder({ ...this.config, url });
  }

  method<M extends string>(method: M): RequestBuilder<T & { method: M }> {
    return new RequestBuilder({ ...this.config, method });
  }

  body<B>(body: B): RequestBuilder<T & { body: B }> {
    return new RequestBuilder({ ...this.config, body });
  }

  build(this: RequestBuilder<{ url: string; method: string }>): T {
    return this.config;
  }
}

const request = new RequestBuilder({})
  .url('/api/users')
  .method('POST')
  .body({ name: 'John' })
  .build();  // ✅ All required fields present

// const incomplete = new RequestBuilder({})
//   .url('/api/users')
//   .build();  // ❌ Error: method is required

Type-Level Programming

Recursive Types

type Json =
  | string
  | number
  | boolean
  | null
  | Json[]
  | { [key: string]: Json };

const data: Json = {
  name: "John",
  age: 30,
  hobbies: ["reading", "coding"],
  address: {
    city: "NYC",
    zip: 10001
  }
};

Type-Level Functions

// Get function parameter types
type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

function greet(name: string, age: number) {
  return `Hello ${name}, ${age}`;
}

type GreetParams = Parameters<typeof greet>;
// [name: string, age: number]

Real-World Example

// API Response Handler
type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

async function fetchUser(id: string): Promise<ApiResponse<User>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    const data = await response.json();
    return { status: 'success', data };
  } catch (error) {
    return { status: 'error', error: error.message };
  }
}

// Type-safe usage
const result = await fetchUser('123');
if (result.status === 'success') {
  console.log(result.data.name);  // ✅ TypeScript knows data exists
} else {
  console.error(result.error);     // ✅ TypeScript knows error exists
}

Best Practices

  1. Use strict mode: Enable all strict options in tsconfig.json
  2. Prefer type inference: Let TypeScript infer types when possible
  3. Avoid any: Use unknown instead
  4. Use const assertions: For literal types
  5. Leverage utility types: Don’t reinvent the wheel
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

Conclusion

TypeScript’s advanced type system enables you to write safer, more maintainable code. Master these concepts to catch errors at compile-time, improve code documentation, and enhance developer experience.

TypeScriptJavaScriptWeb DevelopmentType SafetyProgramming

Related Articles

Web Development

React Performance Optimization: Tips and Tricks for 2025

Master React performance optimization with modern techniques including memoization, code splitting, and concurrent features...

June 10, 2025
6 min
Read More
Web Development

Next.js App Router: Complete Guide to Modern React Development

Master Next.js App Router with server components, streaming, and modern patterns for building fast, SEO-friendly React applications...

December 15, 2024
10 min
Read More
Web Development

CSS Grid vs Flexbox: When to Use Each Layout System

Master modern CSS layouts by understanding when to use Grid vs Flexbox with practical examples and real-world use cases...

October 20, 2024
7 min
Read More