You are viewing a preview of this lesson. Sign in to start learning
Back to TypeScript

Advanced Types & Modules

Master mapped types, conditional types, template literals, and module systems for complex type manipulations.

Advanced Types & Modules in TypeScript

Master TypeScript's advanced type system with free flashcards and spaced repetition practice. This lesson covers conditional types, mapped types, utility types, template literal types, and module organization—essential concepts for building type-safe, scalable applications.

Welcome to Advanced TypeScript 💻

Welcome to the world of advanced TypeScript features! If you've mastered the basics of TypeScript—interfaces, classes, and generics—you're ready to explore the powerful type manipulation capabilities that make TypeScript a true powerhouse for large-scale applications. In this lesson, we'll dive deep into:

  • Conditional Types: Create types that change based on conditions
  • Mapped Types: Transform existing types systematically
  • Template Literal Types: Build string-based types dynamically
  • Utility Types: Leverage TypeScript's built-in type helpers
  • Module Organization: Structure complex codebases effectively

These advanced features enable you to write more expressive, maintainable, and type-safe code. Let's unlock TypeScript's full potential!

Core Concepts: Advanced Type System 🔧

Conditional Types

Conditional types allow you to create types that depend on a condition, similar to ternary operators in JavaScript. The syntax follows the pattern: T extends U ? X : Y

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

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

💡 Think of it like this: Conditional types are the "if-else" statements of the type system. They let your types make decisions!

The infer Keyword

The infer keyword lets you extract and store types within conditional type expressions:

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

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

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

Here, infer R captures the return type of the function, making it available in the true branch.

🔍 Real-world use case: Extract promise values automatically:

type Unpromise<T> = T extends Promise<infer U> ? U : T;

type Result = Unpromise<Promise<string>>;  // string
type Direct = Unpromise<number>;           // number

Mapped Types

Mapped types transform each property in an existing type according to a rule. They use the syntax: { [K in keyof T]: ... }

type ReadonlyVersion<T> = {
  readonly [K in keyof T]: T[K];
};

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

type ReadonlyUser = ReadonlyVersion<User>;
// { readonly name: string; readonly age: number; }
Key Remapping with as

TypeScript 4.1+ allows you to remap keys during mapping:

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

🧠 Memory device: "MAP" = Modify All Properties systematically.

Template Literal Types

Template literal types build new string literal types using template literal syntax:

type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

You can combine multiple string literal types:

type Color = "red" | "blue" | "green";
type Shade = "light" | "dark";
type ColorVariant = `${Shade}-${Color}`;
// "light-red" | "light-blue" | "light-green" | 
// "dark-red" | "dark-blue" | "dark-green"

💡 Pro tip: Use template literals to create type-safe CSS-in-JS systems or route definitions!

Intrinsic String Manipulation Types

TypeScript provides built-in utility types for string manipulation:

TypeDescriptionExample
Uppercase<T>Converts to uppercase"hello" → "HELLO"
Lowercase<T>Converts to lowercase"WORLD" → "world"
Capitalize<T>Capitalizes first letter"user" → "User"
Uncapitalize<T>Lowercases first letter"Name" → "name"

Utility Types

TypeScript includes many utility types that perform common type transformations:

Partial and Required
interface Config {
  host: string;
  port: number;
  timeout: number;
}

// Make all properties optional
type PartialConfig = Partial<Config>;
// { host?: string; port?: number; timeout?: number; }

// Make all properties required
type RequiredConfig = Required<PartialConfig>;
// { host: string; port: number; timeout: number; }
Pick and Omit
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// Select specific properties
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string; }

// Exclude specific properties
type UserWithoutPassword = Omit<User, "password">;
// { id: number; name: string; email: string; }
Record
type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;

const permissions: Permissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"]
};

🔍 Use case: Record<K, V> is perfect for creating dictionary/map types with specific keys.

Extract and Exclude
type T1 = "a" | "b" | "c" | "d";
type T2 = "a" | "c" | "f";

type T3 = Extract<T1, T2>;  // "a" | "c"
type T4 = Exclude<T1, T2>;  // "b" | "d"

Advanced Module Patterns 📦

Namespace vs Module

Modules (ES6 imports/exports) are the modern approach:

// user.ts
export interface User {
  id: number;
  name: string;
}

export function createUser(name: string): User {
  return { id: Date.now(), name };
}

// main.ts
import { User, createUser } from './user';

Namespaces (formerly "internal modules") organize code within a single file:

namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }
  
  export class EmailValidator implements StringValidator {
    isValid(s: string): boolean {
      return /^[^@]+@[^@]+$/.test(s);
    }
  }
}

let validator = new Validation.EmailValidator();

⚠️ Modern best practice: Prefer ES6 modules over namespaces unless maintaining legacy code.

Module Augmentation

You can extend existing modules with declaration merging:

// Original module
interface Array<T> {
  first(): T | undefined;
}

Array.prototype.first = function() {
  return this[0];
};

const arr = [1, 2, 3];
console.log(arr.first());  // 1 (type-safe!)

💡 Pro tip: Use module augmentation to add type definitions to third-party libraries.

Ambient Modules

Ambient modules declare types for JavaScript libraries without TypeScript definitions:

// declarations.d.ts
declare module 'my-untyped-lib' {
  export function doSomething(value: string): number;
  export const version: string;
}

// Now you can use it with types!
import { doSomething } from 'my-untyped-lib';
Barrel Exports

Create an index.ts file to re-export multiple modules:

// models/index.ts
export * from './user';
export * from './product';
export * from './order';

// Now import from one location
import { User, Product, Order } from './models';

🔍 Organizational benefit: Barrel exports simplify imports and create clear API boundaries.

Detailed Examples 🎯

Example 1: Building a Type-Safe Event System

Let's create an event emitter with complete type safety:

// Define event map
interface EventMap {
  'user:login': { userId: string; timestamp: number };
  'user:logout': { userId: string };
  'data:update': { id: string; data: any };
}

// Extract event names
type EventName = keyof EventMap;

// Type-safe event emitter
class TypedEventEmitter {
  private listeners: {
    [K in EventName]?: Array<(payload: EventMap[K]) => void>;
  } = {};

  on<K extends EventName>(
    event: K,
    callback: (payload: EventMap[K]) => void
  ): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(callback);
  }

  emit<K extends EventName>(event: K, payload: EventMap[K]): void {
    const callbacks = this.listeners[event];
    if (callbacks) {
      callbacks.forEach(cb => cb(payload));
    }
  }
}

// Usage with full type safety
const emitter = new TypedEventEmitter();

emitter.on('user:login', (payload) => {
  console.log(`User ${payload.userId} logged in`);
  // payload is correctly typed as { userId: string; timestamp: number }
});

emitter.emit('user:login', { 
  userId: '123', 
  timestamp: Date.now() 
});  // ✓ Type-safe!

// TypeScript error: Property 'wrongField' does not exist
// emitter.emit('user:login', { wrongField: true });

Why this works: The mapped type creates a listener object where each key corresponds to an event name, and the value is an array of callbacks expecting the correct payload type.

Example 2: Deep Readonly Type

Create a utility type that makes an object deeply immutable:

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K];
};

interface Config {
  server: {
    host: string;
    port: number;
    ssl: {
      enabled: boolean;
      cert: string;
    };
  };
  features: string[];
}

type ImmutableConfig = DeepReadonly<Config>;

const config: ImmutableConfig = {
  server: {
    host: 'localhost',
    port: 3000,
    ssl: {
      enabled: true,
      cert: '/path/to/cert'
    }
  },
  features: ['auth', 'logging']
};

// All of these produce TypeScript errors:
// config.server.port = 8080;
// config.server.ssl.enabled = false;
// config.features.push('new-feature');

How it works: The type recursively applies readonly to all properties. It checks if a property is an object (excluding functions) and applies DeepReadonly again, creating nested immutability.

Example 3: Discriminated Union Refiner

Build a utility type that narrows discriminated unions:

type Action =
  | { type: 'INCREMENT'; payload: number }
  | { type: 'DECREMENT'; payload: number }
  | { type: 'RESET' }
  | { type: 'SET_VALUE'; payload: { value: number; source: string } };

// Extract action by type
type ActionByType<T extends Action['type']> = Extract<
  Action,
  { type: T }
>;

type IncrementAction = ActionByType<'INCREMENT'>;
// { type: 'INCREMENT'; payload: number }

type SetValueAction = ActionByType<'SET_VALUE'>;
// { type: 'SET_VALUE'; payload: { value: number; source: string } }

// Create type-safe reducer
function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'INCREMENT':
      return state + action.payload;  // payload is number
    case 'DECREMENT':
      return state - action.payload;  // payload is number
    case 'RESET':
      return 0;  // no payload available
    case 'SET_VALUE':
      return action.payload.value;  // payload has correct structure
    default:
      return state;
  }
}

Pattern explanation: Extract filters the union type to only include actions with the specified type property, giving you perfect autocomplete and type checking.

Example 4: Builder Pattern with Type State

Implement a builder pattern that enforces method call order at compile time:

interface QueryBuilder<
  TSelect extends boolean = false,
  TWhere extends boolean = false,
  TOrderBy extends boolean = false
> {
  select<T>(fields: T[]): QueryBuilder<true, TWhere, TOrderBy>;
  where(condition: string): QueryBuilder<TSelect, true, TOrderBy>;
  orderBy(field: string): QueryBuilder<TSelect, TWhere, true>;
  
  // Only available when all required steps are complete
  execute: TSelect extends true
    ? TWhere extends true
      ? () => Promise<any[]>
      : never
    : never;
}

function createQuery(): QueryBuilder<false, false, false> {
  return {
    select(fields) {
      console.log('SELECT', fields);
      return this as any;
    },
    where(condition) {
      console.log('WHERE', condition);
      return this as any;
    },
    orderBy(field) {
      console.log('ORDER BY', field);
      return this as any;
    },
    execute: undefined as any
  };
}

const query = createQuery();

// This works:
const result = query
  .select(['name', 'email'])
  .where('age > 18')
  .execute();  // ✓ Available

// This doesn't compile:
// const invalid = createQuery()
//   .select(['name'])
//   .execute();  // ✗ Error: where() not called yet

Type-state pattern: The generic parameters track which methods have been called, preventing invalid sequences at compile time.

Common Mistakes ⚠️

Mistake 1: Overusing any in Conditional Types

Wrong approach:

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

Better approach:

type GoodExtract<T> = T extends (...args: infer P) => infer R 
  ? (...args: P) => R 
  : never;

Why: Using infer preserves exact parameter and return types instead of losing them to any.

Mistake 2: Forgetting readonly in Mapped Types

Missing immutability:

type Config<T> = {
  [K in keyof T]: T[K];  // Properties are still mutable!
};

Enforce immutability:

type Config<T> = {
  readonly [K in keyof T]: T[K];
};

Mistake 3: Circular Type References

Infinite recursion:

type JSONValue = 
  | string 
  | number 
  | boolean 
  | JSONValue[]  // This can cause issues
  | { [key: string]: JSONValue };

Proper recursive type:

type JSONPrimitive = string | number | boolean | null;
type JSONValue = JSONPrimitive | JSONArray | JSONObject;
interface JSONArray extends Array<JSONValue> {}
interface JSONObject { [key: string]: JSONValue; }

Fix explanation: Using interfaces for recursive structures helps TypeScript handle circularity better.

Mistake 4: Not Understanding Type Distribution

Conditional types distribute over unions:

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

type Result = ToArray<string | number>;
// string[] | number[] (NOT (string | number)[])

💡 To prevent distribution, use square brackets:

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

type Result2 = ToArrayNoDistribute<string | number>;
// (string | number)[]

Mistake 5: Improper Module Organization

Circular dependencies:

// user.ts
import { Order } from './order';
export interface User { orders: Order[]; }

// order.ts
import { User } from './user';
export interface Order { user: User; }  // Circular!

Break the cycle:

// types.ts
export interface User { id: string; orders: string[]; }
export interface Order { id: string; userId: string; }

// user.ts
import { User } from './types';

// order.ts
import { Order } from './types';

Key Takeaways 🎓

📋 Quick Reference Card

ConceptSyntaxUse Case
Conditional TypesT extends U ? X : YType decisions based on conditions
Infer KeywordT extends (arg: infer P) => anyExtract types from patterns
Mapped Types{[K in keyof T]: ...}Transform all properties systematically
Key Remapping[K in keyof T as NewK]Rename keys during mapping
Template Literals`${A}-${B}`Build string-based types
Utility TypesPartial, Pick, Omit, RecordCommon type transformations
Module Exportexport { Type, function }Share types and values
Declaration Merginginterface X { ... } (multiple times)Extend existing types

Essential Patterns to Remember

  1. Conditional types with infer unlock powerful type extraction capabilities
  2. Mapped types let you transform entire object structures systematically
  3. Template literal types enable type-safe string manipulation and generation
  4. Utility types save time—learn them instead of recreating common patterns
  5. Module organization matters—prefer ES6 modules over namespaces
  6. Type-state pattern enforces correctness at compile time, not runtime
  7. Declaration merging extends existing types without modification

🔍 Real-world principle: Advanced types move errors from runtime to compile time, making your code more reliable and maintainable.

📚 Further Study

  1. TypeScript Handbook - Advanced Types: https://www.typescriptlang.org/docs/handbook/2/types-from-types.html - Official documentation with comprehensive examples
  2. Type Challenges: https://github.com/type-challenges/type-challenges - Practice advanced TypeScript with real-world challenges
  3. TypeScript Deep Dive: https://basarat.gitbook.io/typescript/ - Free online book covering advanced patterns and best practices

🎉 Congratulations! You've mastered advanced TypeScript types and modules. These powerful features will help you build more robust, type-safe applications with confidence. Keep practicing with real-world scenarios to internalize these patterns!