Back to blog
Advertisement

7 Advanced TypeScript Techniques You Should Master in 2025

Hello HaWkers, TypeScript has established itself as the standard choice for serious JavaScript projects. In 2025, over 78% of new web projects use TypeScript from the start, according to recent community surveys.

Do you master the basics of TypeScript but feel like you're just scratching the surface? This article goes beyond interfaces and basic types, exploring techniques that separate intermediate developers from TypeScript experts.

1. Strict Mode: The Foundation of Safe Code

TypeScript's strict mode isn't a single technique but a set of flags that make your code drastically safer. In 2025, projects without strict: true are considered legacy.

// tsconfig.json - Modern configuration
{
  "compilerOptions": {
    "strict": true, // Activates all strict flags
    "noUncheckedIndexedAccess": true, // TS 5.0+
    "noPropertyAccessFromIndexSignature": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true
  }
}

What many don't realize is that strict: true activates several flags individually:

  • noImplicitAny: Prohibits implicit any
  • strictNullChecks: null and undefined are distinct types
  • strictFunctionTypes: Contravariant parameter checking
  • strictBindCallApply: Correct typing for .bind(), .call(), .apply()
  • strictPropertyInitialization: Class properties must be initialized
  • noImplicitThis: this must have explicit type in ambiguous contexts

But the real power comes from additional flags like noUncheckedIndexedAccess, introduced in recent versions.

// Without noUncheckedIndexedAccess
const users: Record<string, User> = {};
const user = users['123']; // Type: User
user.name; // ❌ Runtime error: Cannot read property 'name' of undefined

// With noUncheckedIndexedAccess
const users: Record<string, User> = {};
const user = users['123']; // Type: User | undefined
// user.name; // ❌ Compilation error
user?.name; // ✅ Safe

This flag alone prevents countless production bugs related to accessing objects that may not exist.

Advertisement

2. Template Literal Types: Powerful Dynamic Types

Template Literal Types, introduced in TypeScript 4.1 and expanded in later versions, allow you to create dynamic string-based types.

// Creating dynamic types for events
type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// Type: 'onClick' | 'onFocus' | 'onBlur'

type EventHandlers = {
  [K in HandlerName]: (event: Event) => void;
};

// Practical usage
const handlers: EventHandlers = {
  onClick: (e) => console.log('Clicked'),
  onFocus: (e) => console.log('Focused'),
  onBlur: (e) => console.log('Blurred')
};

This is extremely powerful for APIs that follow naming conventions. Here's a real example with API routes:

// Type-safe routing system
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Resource = 'user' | 'product' | 'order';

type Route = `/${Resource}` | `/${Resource}/${string}`;
type RouteHandler<M extends HTTPMethod, R extends Route> = {
  method: M;
  route: R;
  handler: (req: Request) => Promise<Response>;
};

// Usage with full autocomplete
const userRoute: RouteHandler<'GET', '/user'> = {
  method: 'GET',
  route: '/user',
  handler: async (req) => {
    // Implementation
    return new Response();
  }
};

// ❌ Compilation error: 'PATCH' doesn't exist in HTTPMethod
// const invalidRoute: RouteHandler<'PATCH', '/user'> = { ... };

typescript types autocomplete

Advertisement

3. Conditional Types and Inference: Logic in Types

Conditional Types allow you to create complex logic in the type system, similar to ternary operators in JavaScript.

// Type that extracts the return type from a Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number

// Practical application: function that returns Promise or direct value
async function fetchData<T>(
  useCache: boolean,
  data: T
): Promise<UnwrapPromise<T>> {
  if (useCache) {
    return data as UnwrapPromise<T>;
  }
  // Simulates async fetch
  return new Promise(resolve =>
    setTimeout(() => resolve(data as UnwrapPromise<T>), 100)
  ) as any;
}

An advanced pattern is combining conditional types with mapped types:

// Transforms optional properties to required and vice versa
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type RequiredKeys<T, K extends keyof T> = T & {
  [P in K]-?: T[P];
};

interface User {
  id: string;
  name?: string;
  email?: string;
}

// Makes name and email required
type FullUser = RequiredKeys<User, 'name' | 'email'>;
// Type: { id: string; name: string; email: string; }

This pattern is incredibly useful for form validation and DTOs where certain properties become required in specific contexts.

4. Branded Types: Safety in Primitive Types

Branded Types (or Nominal Types) solve a common problem: how to differentiate strings with different meanings?

// Problem: userId and productId are both strings
function getUser(id: string) { /* ... */ }
function getProduct(id: string) { /* ... */ }

const userId = "user123";
const productId = "prod456";

getUser(productId); // ❌ Bug, but TypeScript doesn't complain

Solution with Branded Types:

// Creating branded types
type Brand<K, T> = K & { __brand: T };

type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;

// Factory functions (smart constructors)
function createUserId(id: string): UserId {
  // Validation if necessary
  return id as UserId;
}

function createProductId(id: string): ProductId {
  return id as ProductId;
}

// Type-safe usage
function getUser(id: UserId) { /* ... */ }
function getProduct(id: ProductId) { /* ... */ }

const userId = createUserId("user123");
const productId = createProductId("prod456");

getUser(userId); // ✅ OK
// getUser(productId); // ❌ Compilation error!

This is especially valuable in financial systems where you work with different currencies or IDs from different domains.

Advertisement

5. Advanced Utility Types: Beyond Partial and Pick

TypeScript offers powerful utility types, but most developers only use Partial, Pick, and Omit. Let's go beyond:

// ReturnType: Extracts function return type
function createUser() {
  return {
    id: '123',
    name: 'John',
    createdAt: new Date()
  };
}

type User = ReturnType<typeof createUser>;
// Type: { id: string; name: string; createdAt: Date; }

// Parameters: Extracts function parameter types
function updateUser(id: string, data: { name: string; email: string }) {
  // Implementation
}

type UpdateUserParams = Parameters<typeof updateUser>;
// Type: [id: string, data: { name: string; email: string; }]

// Awaited: Unwrap Promise types (TS 4.5+)
type Response = Awaited<Promise<{ data: string }>>;
// Type: { data: string; }

// Combining utility types
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;
type UserKeys = keyof User; // 'id' | 'name' | 'createdAt'

A powerful pattern is creating your own custom utility types:

// DeepPartial: Makes all nested properties optional
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? DeepPartial<T[P]>
    : T[P];
};

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      user: string;
      password: string;
    };
  };
  cache: {
    ttl: number;
  };
}

// Allows deep partial updates
function updateConfig(config: DeepPartial<Config>) {
  // You can pass only what you want to update
}

updateConfig({
  database: {
    credentials: {
      password: 'new-password' // Only password, rest is optional
    }
  }
});
Advertisement

6. Discriminated Unions: Automatic Type Guards

Discriminated Unions (or Tagged Unions) are a pattern that makes impossible states impossible to represent.

// Request state using discriminated union
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function handleRequest<T>(state: RequestState<T>) {
  // TypeScript narrows the type automatically
  switch (state.status) {
    case 'idle':
      // state.status is 'idle'
      // state.data doesn't exist (compilation error if you try to access)
      return 'Waiting...';

    case 'loading':
      return 'Loading...';

    case 'success':
      // TypeScript knows state.data exists here!
      return `Success: ${state.data}`;

    case 'error':
      // TypeScript knows state.error exists here!
      return `Error: ${state.error.message}`;
  }
}

// Usage in React components
function UserProfile() {
  const [userState, setUserState] =
    useState<RequestState<User>>({ status: 'idle' });

  useEffect(() => {
    setUserState({ status: 'loading' });

    fetchUser()
      .then(data => setUserState({ status: 'success', data }))
      .catch(error => setUserState({ status: 'error', error }));
  }, []);

  return <div>{handleRequest(userState)}</div>;
}

This pattern eliminates common bugs where you access data when in loading state, or try to show error when you actually have success.

7. Type Predicates: Custom Type Guards

Type predicates allow you to create functions that inform TypeScript about the type of a value at runtime.

// Basic type predicate
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

// Usage
function processValue(value: string | number) {
  if (isString(value)) {
    // TypeScript knows value is string here
    console.log(value.toUpperCase());
  } else {
    // TypeScript knows value is number here
    console.log(value.toFixed(2));
  }
}

// Advanced type predicate with validation
interface User {
  id: string;
  name: string;
  email: string;
}

function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'name' in obj &&
    'email' in obj &&
    typeof (obj as any).id === 'string' &&
    typeof (obj as any).name === 'string' &&
    typeof (obj as any).email === 'string'
  );
}

// Usage with API responses
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  if (!isUser(data)) {
    throw new Error('Invalid user data from API');
  }

  // TypeScript knows data is User here
  return data;
}

typescript validation

Advertisement

Bonus: Integration with Zod for Runtime Validation

TypeScript checks types at compile-time, but APIs return data at runtime. Zod closes this gap:

import { z } from 'zod';

// Zod schema
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().positive().optional()
});

// TypeScript type automatically inferred
type User = z.infer<typeof UserSchema>;

// Runtime validation with type narrowing
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  // Parse validates and returns User or throws error
  return UserSchema.parse(data);
}

// Safe validation without exceptions
const result = UserSchema.safeParse(unknownData);

if (result.success) {
  // result.data is User
  console.log(result.data.name);
} else {
  // result.error contains details
  console.error(result.error.errors);
}

This combination of TypeScript with Zod is the gold standard in 2025 for applications that deal with external data.

The Real Impact of These Techniques

Implementing these techniques isn't about writing "prettier" code — it's about preventing bugs. In a 2024 study, teams that adopted strict mode + branded types + discriminated unions reported:

  • 38% fewer production bugs related to incorrect types
  • 22% reduction in debugging time
  • 15% increase in confidence when refactoring code

Well-used TypeScript transforms your editor into an assistant that prevents errors even before you run the code.

Want to better understand the JavaScript fundamentals that make TypeScript possible? Check out my article on Functional Programming in JavaScript where you'll discover concepts that TypeScript uses for type inference.

Let's go! 🦅

📚 Want to Deepen Your JavaScript Knowledge?

This article covered advanced TypeScript techniques, but there's much more to explore in modern development.

Developers who invest in solid, structured knowledge tend to have more opportunities in the market.

Complete Study Material

If you want to master JavaScript from basics to advanced, I've prepared a complete guide:

Investment options:

  • 2x of $13.08 on card
  • or $24.90 at sight

👉 Learn About JavaScript Guide

💡 Material updated with industry best practices

Advertisement
Previous postNext post

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments