Skip to main content

🎨 Lesson 10.2: TypeScript Advanced Patterns

Unlock the full power of TypeScript with advanced patterns for building type-safe, reusable React components. Master generics, discriminated unions, and utility types to create robust component libraries.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Use generics to create flexible, reusable React components
  • Implement discriminated unions for type-safe state management
  • Apply type guards to narrow types safely at runtime
  • Leverage utility types to transform component props
  • Create conditional types for advanced type logic
  • Use template literal types for precise string typing

Estimated Time: 75-90 minutes

Project: Build a type-safe component library

πŸ“‘ In This Lesson

πŸ“– Introduction to Advanced TypeScript

You've built amazing React applications with TypeScript, mastering the fundamentals along the way. Now it's time to level up your TypeScript skills with advanced patterns that professional developers use to build maintainable, scalable codebases.

These patterns aren't just about writing "clever" codeβ€”they're about creating components that are impossible to use incorrectly, catching bugs at compile time instead of runtime, and making your codebase easier to understand and maintain.

πŸ“– Why Advanced TypeScript Matters

Advanced TypeScript patterns help you:

  • Catch bugs earlier: Move error detection from runtime to compile time
  • Build reusable components: Create flexible components that work with any data type
  • Improve developer experience: Better autocomplete and inline documentation
  • Reduce tests: Types prove correctness, reducing the need for certain tests
  • Refactor confidently: Type checker ensures changes don't break code

🎯 Real-World Scenario

Imagine building a data table component. Without advanced types, you might write:

// 😱 Weak typing - easy to make mistakes
function DataTable({ data, columns }: any) {
  return (
    <table>
      {/* Implementation */}
    </table>
  );
}

// Usage - NO type safety!
<DataTable 
  data={users} 
  columns={['name', 'emial']} // Typo! Runtime error
/>

With advanced TypeScript patterns, you can make this completely type-safe:

// βœ… Strong typing - catches errors at compile time
function DataTable<T>({ 
  data, 
  columns 
}: { 
  data: T[]; 
  columns: (keyof T)[] 
}) {
  return (
    <table>
      {/* Implementation */}
    </table>
  );
}

// Usage - Fully type-safe!
<DataTable 
  data={users} 
  columns={['name', 'emial']} // Error: 'emial' doesn't exist!
/>

βœ… The Advanced TypeScript Mindset

Think of TypeScript's type system as a proof system. You're not just adding annotationsβ€”you're proving to the compiler that your code is correct. The more precise your types, the more bugs you catch before users see them.

🧬 Generics in React Components

Generics are TypeScript's most powerful feature for creating reusable components. They allow you to write components that work with any type while maintaining full type safety.

πŸ“š Understanding Generics

Think of generics as type parametersβ€”variables for types instead of values:

// Generic function - works with any type
function identity<T>(value: T): T {
  return value;
}

const num = identity(42);        // T is number
const str = identity('hello');   // T is string
const obj = identity({ x: 10 }); // T is { x: number }

πŸ“– Generic Syntax

<T> declares a type parameter named T. You can use any name, but conventions are:

  • T - Generic Type
  • K - Key type
  • V - Value type
  • E - Element type
  • P - Props type

🎯 Generic React Components

Let's build practical generic components:

Example 1: Generic List Component

// Generic List component - works with any data type
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  emptyMessage?: string;
}

function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) {
  if (items.length === 0) {
    return <div className="empty">{emptyMessage || 'No items'}</div>;
  }
  
  return (
    <div className="list">
      {items.map((item, index) => (
        <div key={keyExtractor(item)}>
          {renderItem(item, index)}
        </div>
      ))}
    </div>
  );
}

// Usage with different types
interface User {
  id: number;
  name: string;
  email: string;
}

interface Product {
  id: string;
  title: string;
  price: number;
}

function UserList({ users }: { users: User[] }) {
  return (
    <List
      items={users}
      renderItem={(user) => ( // user is typed as User!
        <div>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      )}
      keyExtractor={(user) => user.id}
    />
  );
}

function ProductList({ products }: { products: Product[] }) {
  return (
    <List
      items={products}
      renderItem={(product) => ( // product is typed as Product!
        <div>
          <h3>{product.title}</h3>
          <p>${product.price}</p>
        </div>
      )}
      keyExtractor={(product) => product.id}
    />
  );
}

πŸ’‘ Type Inference with Generics

Notice we didn't need to write <List<User>>. TypeScript inferred the type parameter from the items prop! This makes the API cleaner while maintaining full type safety.

Example 2: Generic Select Component

interface SelectOption<T> {
  value: T;
  label: string;
  disabled?: boolean;
}

interface SelectProps<T> {
  options: SelectOption<T>[];
  value: T;
  onChange: (value: T) => void;
  placeholder?: string;
}

function Select<T extends string | number>({
  options,
  value,
  onChange,
  placeholder
}: SelectProps<T>) {
  return (
    <select
      value={value as string | number}
      onChange={(e) => {
        const newValue = options.find(
          opt => String(opt.value) === e.target.value
        )?.value;
        if (newValue !== undefined) {
          onChange(newValue);
        }
      }}
    >
      {placeholder && (
        <option value="" disabled>
          {placeholder}
        </option>
      )}
      {options.map((option) => (
        <option
          key={String(option.value)}
          value={option.value as string | number}
          disabled={option.disabled}
        >
          {option.label}
        </option>
      ))}
    </select>
  );
}

// Usage with string values
type UserRole = 'admin' | 'editor' | 'viewer';

function RoleSelector() {
  const [role, setRole] = useState<UserRole>('viewer');
  
  return (
    <Select
      options={[
        { value: 'admin', label: 'Administrator' },
        { value: 'editor', label: 'Editor' },
        { value: 'viewer', label: 'Viewer' }
      ]}
      value={role}
      onChange={setRole} // Fully typed!
    />
  );
}

// Usage with number values
function PrioritySelector() {
  const [priority, setPriority] = useState(1);
  
  return (
    <Select
      options={[
        { value: 1, label: 'Low' },
        { value: 2, label: 'Medium' },
        { value: 3, label: 'High' }
      ]}
      value={priority}
      onChange={setPriority} // Also fully typed!
    />
  );
}

βœ… Generic Constraints

Notice <T extends string | number>? This is a generic constraint. It says "T can be any type, but it must be either string or number." This prevents using the Select with invalid types like objects or arrays.

Example 3: Generic Data Fetching Hook

interface UseDataOptions<T> {
  url: string;
  transform?: (data: unknown) => T;
}

interface UseDataResult<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => Promise<void>;
}

function useData<T>({ url, transform }: UseDataOptions<T>): UseDataResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const json = await response.json();
      const transformedData = transform ? transform(json) : json as T;
      
      setData(transformedData);
    } catch (err) {
      setError(err instanceof Error ? err : new Error('Unknown error'));
    } finally {
      setLoading(false);
    }
  }, [url, transform]);
  
  useEffect(() => {
    fetchData();
  }, [fetchData]);
  
  return { data, loading, error, refetch: fetchData };
}

// Usage with different types
interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error } = useData<User>({
    url: `/api/users/${userId}`
  });
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>No user found</div>;
  
  return (
    <div>
      <h1>{user.name}</h1> {/* Fully typed! */}
      <p>{user.email}</p>
    </div>
  );
}

function PostList() {
  const { data: posts, loading } = useData<Post[]>({
    url: '/api/posts'
  });
  
  if (loading) return <div>Loading...</div>;
  if (!posts) return <div>No posts</div>;
  
  return (
    <div>
      {posts.map(post => ( // post is typed as Post!
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}

⚠️ When to Use Generics

Use generics when:

  • The component works with multiple data types
  • You want to maintain type information through the component
  • You're building reusable library components

Don't use generics when:

  • The component only works with one specific type
  • It makes the code harder to understand
  • Simple prop types would suffice

🎯 Multiple Type Parameters

Sometimes you need more than one type parameter:

// Generic Map component with key and value types
interface MapProps<K, V> {
  items: Map<K, V>;
  renderKey: (key: K) => React.ReactNode;
  renderValue: (value: V) => React.ReactNode;
}

function MapDisplay<K, V>({ items, renderKey, renderValue }: MapProps<K, V>) {
  return (
    <div>
      {Array.from(items.entries()).map(([key, value]) => (
        <div key={String(key)} className="map-item">
          <div className="key">{renderKey(key)}</div>
          <div className="value">{renderValue(value)}</div>
        </div>
      ))}
    </div>
  );
}

// Usage
function SettingsDisplay() {
  const settings = new Map<string, boolean>([
    ['darkMode', true],
    ['notifications', false],
    ['autoSave', true]
  ]);
  
  return (
    <MapDisplay
      items={settings}
      renderKey={(key) => <strong>{key}</strong>}
      renderValue={(value) => <span>{value ? 'On' : 'Off'}</span>}
    />
  );
}
graph LR A[Component with Generics] --> B[Type Parameter T] B --> C[Props use T] B --> D[State uses T] B --> E[Return type uses T] C --> F[Full Type Safety] D --> F E --> F F --> G[Autocomplete] F --> H[Error Detection] F --> I[Refactoring Support] style A fill:#667eea,color:#fff style F fill:#c8e6c9 style G fill:#e8f5e9 style H fill:#e8f5e9 style I fill:#e8f5e9

πŸ”€ Discriminated Unions

Discriminated unions (also called tagged unions) are one of TypeScript's most powerful features for modeling data that can be in different states. They're perfect for state machines, API responses, and form states.

πŸ“š What Are Discriminated Unions?

A discriminated union is a union type where each member has a common property (the "discriminant") with a literal type:

// Each type has a 'type' discriminant with a literal value
type Success = {
  type: 'success';
  data: string;
};

type Error = {
  type: 'error';
  message: string;
};

type Loading = {
  type: 'loading';
};

// Union of all possible states
type ApiState = Success | Error | Loading;

// TypeScript can narrow the type based on the discriminant!
function handleState(state: ApiState) {
  switch (state.type) {
    case 'success':
      // TypeScript knows this is Success
      console.log(state.data); // βœ… OK
      break;
    case 'error':
      // TypeScript knows this is Error
      console.log(state.message); // βœ… OK
      break;
    case 'loading':
      // TypeScript knows this is Loading
      console.log('Loading...'); // βœ… OK
      break;
  }
}

πŸ“– Why Discriminated Unions?

Discriminated unions provide:

  • Exhaustive checking: TypeScript ensures you handle all cases
  • Type narrowing: Automatic type inference in each branch
  • Impossible states: Prevent invalid state combinations
  • Self-documenting: The types show all possible states

🎯 Practical Example: Async Data States

// 😱 Bad: Multiple booleans = impossible states
interface BadAsyncState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  // Problem: Can be loading AND have error AND have data!
}

// βœ… Good: Discriminated union = impossible to have invalid state
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

// Component using discriminated union
interface DataDisplayProps<T> {
  state: AsyncState<T>;
  renderData: (data: T) => React.ReactNode;
}

function DataDisplay<T>({ state, renderData }: DataDisplayProps<T>) {
  switch (state.status) {
    case 'idle':
      return <div>Click fetch to load data</div>;
    
    case 'loading':
      return <div>Loading...</div>;
    
    case 'success':
      // TypeScript knows state.data exists here!
      return <div>{renderData(state.data)}</div>;
    
    case 'error':
      // TypeScript knows state.error exists here!
      return <div>Error: {state.error.message}</div>;
    
    default:
      // Exhaustiveness check - TypeScript errors if we miss a case
      const _exhaustive: never = state;
      return _exhaustive;
  }
}

// Usage
function UserProfile() {
  const [state, setState] = useState<AsyncState<User>>({ status: 'idle' });
  
  const fetchUser = async () => {
    setState({ status: 'loading' });
    
    try {
      const response = await fetch('/api/user');
      const data = await response.json();
      setState({ status: 'success', data });
    } catch (error) {
      setState({ 
        status: 'error', 
        error: error instanceof Error ? error : new Error('Unknown error')
      });
    }
  };
  
  return (
    <div>
      <button onClick={fetchUser}>Fetch User</button>
      <DataDisplay
        state={state}
        renderData={(user) => (
          <div>
            <h2>{user.name}</h2>
            <p>{user.email}</p>
          </div>
        )}
      />
    </div>
  );
}

πŸ’‘ The Never Type Trick

The const _exhaustive: never = state; line is a TypeScript trick for exhaustiveness checking. If you forget to handle a case, TypeScript will error because that case can't be assigned to never. This ensures you handle all possible states!

🎯 Practical Example: Form States

// Model all possible form states
type FormState<T> =
  | { status: 'editing'; values: Partial<T>; errors: Partial<Record<keyof T, string>> }
  | { status: 'submitting'; values: T }
  | { status: 'success'; submittedValues: T }
  | { status: 'failed'; values: T; error: string };

interface LoginForm {
  email: string;
  password: string;
}

function LoginComponent() {
  const [formState, setFormState] = useState<FormState<LoginForm>>({
    status: 'editing',
    values: {},
    errors: {}
  });
  
  const handleSubmit = async (values: LoginForm) => {
    setFormState({ status: 'submitting', values });
    
    try {
      await loginUser(values);
      setFormState({ status: 'success', submittedValues: values });
    } catch (error) {
      setFormState({
        status: 'failed',
        values,
        error: error instanceof Error ? error.message : 'Login failed'
      });
    }
  };
  
  // Render based on state
  switch (formState.status) {
    case 'editing':
      return (
        <form onSubmit={(e) => {
          e.preventDefault();
          if (isValid(formState.values)) {
            handleSubmit(formState.values as LoginForm);
          }
        }}>
          {/* Form fields with errors from formState.errors */}
        </form>
      );
    
    case 'submitting':
      return <div>Logging in...</div>;
    
    case 'success':
      return <div>Welcome, {formState.submittedValues.email}!</div>;
    
    case 'failed':
      return (
        <div>
          <div className="error">{formState.error}</div>
          <button onClick={() => setFormState({
            status: 'editing',
            values: formState.values,
            errors: {}
          })}>
            Try Again
          </button>
        </div>
      );
  }
}

βœ… Benefits of This Pattern

  • Impossible to be "submitting" and "editing" at the same time
  • Can't access error unless status is 'failed'
  • Clear state transitions
  • Easy to add new states without breaking existing code

πŸ›‘οΈ Type Guards and Narrowing

Type guards are functions or expressions that perform runtime checks to narrow types. They bridge the gap between compile-time types and runtime values, making your code safer and more expressive.

πŸ“š What Are Type Guards?

Type guards tell TypeScript to narrow a broad type to a more specific type based on a runtime check:

// Without type guard - TypeScript doesn't know the specific type
function processValue(value: string | number) {
  // Error: Property 'toFixed' does not exist on type 'string | number'
  console.log(value.toFixed(2));
}

// With type guard - TypeScript narrows the type
function processValue(value: string | number) {
  if (typeof value === 'number') {
    // TypeScript knows value is number here
    console.log(value.toFixed(2)); // βœ… OK
  } else {
    // TypeScript knows value is string here
    console.log(value.toUpperCase()); // βœ… OK
  }
}

πŸ“– Built-in Type Guards

TypeScript has several built-in type guards:

  • typeof - For primitive types (string, number, boolean, etc.)
  • instanceof - For class instances
  • in - For checking if a property exists
  • Array.isArray() - For arrays

🎯 Using typeof

function formatValue(value: string | number | boolean): string {
  if (typeof value === 'string') {
    return value.toUpperCase();
  } else if (typeof value === 'number') {
    return value.toFixed(2);
  } else {
    return value ? 'Yes' : 'No';
  }
}

// React component example
interface TextOrNumberProps {
  value: string | number;
  prefix?: string;
}

function DisplayValue({ value, prefix }: TextOrNumberProps) {
  return (
    
{prefix && {prefix}: } {typeof value === 'number' ? ( {value.toLocaleString()} ) : ( {value} )}
); }

🎯 Using instanceof

class ApiError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public endpoint: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

class ValidationError extends Error {
  constructor(
    message: string,
    public field: string
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

// Type guard with instanceof
function ErrorDisplay({ error }: { error: Error }) {
  if (error instanceof ApiError) {
    // TypeScript knows error is ApiError here
    return (
      

API Error ({error.statusCode})

{error.message}

Endpoint: {error.endpoint}
); } if (error instanceof ValidationError) { // TypeScript knows error is ValidationError here return (

Validation Error

{error.message}

Field: {error.field}
); } // Generic error return (

Error

{error.message}

); }

🎯 Using the 'in' Operator

interface Bird {
  type: 'bird';
  wingspan: number;
  fly: () => void;
}

interface Fish {
  type: 'fish';
  swimSpeed: number;
  swim: () => void;
}

type Animal = Bird | Fish;

// Type guard using 'in' operator
function moveAnimal(animal: Animal) {
  if ('fly' in animal) {
    // TypeScript knows animal is Bird
    animal.fly();
    console.log(`Wingspan: ${animal.wingspan}m`);
  } else {
    // TypeScript knows animal is Fish
    animal.swim();
    console.log(`Swim speed: ${animal.swimSpeed}km/h`);
  }
}

// React component example
function AnimalCard({ animal }: { animal: Animal }) {
  return (
    
{'fly' in animal ? (

🐦 Bird

Wingspan: {animal.wingspan}m

) : (

🐟 Fish

Speed: {animal.swimSpeed}km/h

)}
); }

🎯 Custom Type Guard Functions

For complex types, create custom type guard functions using type predicates:

interface User {
  type: 'user';
  id: number;
  name: string;
  email: string;
}

interface Guest {
  type: 'guest';
  sessionId: string;
}

type Visitor = User | Guest;

// Custom type guard with type predicate
function isUser(visitor: Visitor): visitor is User {
  return visitor.type === 'user';
}

function isGuest(visitor: Visitor): visitor is Guest {
  return visitor.type === 'guest';
}

// Usage in component
function VisitorGreeting({ visitor }: { visitor: Visitor }) {
  if (isUser(visitor)) {
    // TypeScript knows visitor is User
    return (
      

Welcome back, {visitor.name}!

Email: {visitor.email}

); } if (isGuest(visitor)) { // TypeScript knows visitor is Guest return (

Welcome, Guest!

Session: {visitor.sessionId}

); } // Exhaustiveness check const _exhaustive: never = visitor; return _exhaustive; }

πŸ’‘ Type Predicate Syntax

The visitor is User syntax is a type predicate. It tells TypeScript: "If this function returns true, then the parameter is of type User." This is more powerful than just returning boolean because it affects TypeScript's type narrowing.

🎯 Advanced: Type Guards with Generics

// Generic type guard for checking if value is defined
function isDefined<T>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null;
}

// Usage
const values = [1, 2, undefined, 4, null, 6];
const definedValues = values.filter(isDefined); // Type: number[]

// Generic type guard for arrays
function isArrayOf<T>(
  value: unknown,
  itemGuard: (item: unknown) => item is T
): value is T[] {
  return Array.isArray(value) && value.every(itemGuard);
}

// Type guard for string
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

// Type guard for number
function isNumber(value: unknown): value is number {
  return typeof value === 'number';
}

// Usage
function processData(data: unknown) {
  if (isArrayOf(data, isString)) {
    // TypeScript knows data is string[]
    data.forEach(str => console.log(str.toUpperCase()));
  } else if (isArrayOf(data, isNumber)) {
    // TypeScript knows data is number[]
    const sum = data.reduce((a, b) => a + b, 0);
    console.log('Sum:', sum);
  }
}

🎯 Assertion Functions

Assertion functions throw errors if a condition isn't met, and TypeScript narrows types accordingly:

// Assertion function
function assertIsUser(visitor: Visitor): asserts visitor is User {
  if (visitor.type !== 'user') {
    throw new Error('Visitor is not a user');
  }
}

function sendEmailToUser(visitor: Visitor) {
  assertIsUser(visitor);
  // After this line, TypeScript knows visitor is User
  sendEmail(visitor.email); // βœ… OK - no type guard needed!
}

// Generic assertion function
function assertIsDefined<T>(value: T | undefined | null): asserts value is T {
  if (value === undefined || value === null) {
    throw new Error('Value is not defined');
  }
}

// Usage in React
function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  
  // Instead of optional chaining everywhere...
  try {
    assertIsDefined(user);
    // Now TypeScript knows user is User (not null)
    
    return (
      

{user.name}

{user.email}

); } catch { return
Loading...
; } }

βœ… Type Guards Best Practices

  • Use built-in type guards (typeof, instanceof, in) when possible
  • Create custom type guards for complex domain types
  • Name type guard functions clearly: isUser, hasProperty
  • Combine type guards with discriminated unions for exhaustive checks
  • Use assertion functions when you're certain about the type

πŸ”§ Utility Types for Components

TypeScript provides powerful utility types that transform existing types. These are essential for building flexible React components without duplicating type definitions.

πŸ“š Built-in Utility Types

graph TD A[Utility Types] --> B[Partial<T>] A --> C[Required<T>] A --> D[Readonly<T>] A --> E[Pick<T, K>] A --> F[Omit<T, K>] A --> G[Record<K, T>] A --> H[Extract<T, U>] A --> I[Exclude<T, U>] B --> B1[Makes all properties optional] C --> C1[Makes all properties required] D --> D1[Makes all properties readonly] E --> E1[Picks specific properties] F --> F1[Omits specific properties] G --> G1[Creates object type with keys] H --> H1[Extracts from union] I --> I1[Excludes from union] style A fill:#667eea,color:#fff style B fill:#c8e6c9 style C fill:#c8e6c9 style D fill:#c8e6c9 style E fill:#c8e6c9 style F fill:#c8e6c9 style G fill:#c8e6c9 style H fill:#c8e6c9 style I fill:#c8e6c9

🎯 Partial<T> - Make All Properties Optional

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

// Partial makes all properties optional
type UserUpdate = Partial<User>;
// Equivalent to:
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   age?: number;
// }

// Practical use: Update functions
function updateUser(id: number, updates: Partial<User>) {
  // Can update any subset of properties
  return fetch(`/api/users/${id}`, {
    method: 'PATCH',
    body: JSON.stringify(updates)
  });
}

// Usage
updateUser(1, { name: 'John' }); // βœ… OK
updateUser(1, { email: 'john@example.com', age: 30 }); // βœ… OK
updateUser(1, { invalid: 'field' }); // ❌ Error

// React component with partial props for editing
interface UserFormProps {
  initialValues?: Partial<User>;
  onSubmit: (user: User) => void;
}

function UserForm({ initialValues = {}, onSubmit }: UserFormProps) {
  const [formData, setFormData] = useState<Partial<User>>(initialValues);
  
  // Form implementation...
  return (
    
{ e.preventDefault(); if (isValidUser(formData)) { onSubmit(formData as User); } }}> {/* Form fields */}
); }

🎯 Required<T> - Make All Properties Required

interface Config {
  apiUrl?: string;
  timeout?: number;
  retries?: number;
}

// Required makes all properties required
type RequiredConfig = Required<Config>;
// Equivalent to:
// {
//   apiUrl: string;
//   timeout: number;
//   retries: number;
// }

// Practical use: Ensure config is complete
function validateConfig(config: Config): config is Required<Config> {
  return config.apiUrl !== undefined 
    && config.timeout !== undefined 
    && config.retries !== undefined;
}

function initializeApp(config: Config) {
  if (!validateConfig(config)) {
    throw new Error('Incomplete configuration');
  }
  
  // TypeScript knows all properties are defined
  console.log(config.apiUrl.toUpperCase()); // βœ… OK
  console.log(config.timeout * 2); // βœ… OK
}

🎯 Readonly<T> - Make All Properties Readonly

interface Point {
  x: number;
  y: number;
}

type ImmutablePoint = Readonly<Point>;
// Equivalent to:
// {
//   readonly x: number;
//   readonly y: number;
// }

// Practical use: Immutable props
interface ChartProps {
  data: Readonly<DataPoint[]>;
  config: Readonly<ChartConfig>;
}

function Chart({ data, config }: ChartProps) {
  // Cannot modify props
  // data.push(newPoint); // ❌ Error
  // config.title = 'New Title'; // ❌ Error
  
  return 
{/* Chart rendering */}
; }

🎯 Pick<T, K> - Select Specific Properties

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  age: number;
  address: string;
}

// Pick only the properties we want
type UserPublicInfo = Pick<User, 'id' | 'name' | 'email'>;
// Equivalent to:
// {
//   id: number;
//   name: string;
//   email: string;
// }

// Practical use: API responses
function getUserPublicProfile(userId: number): Promise<UserPublicInfo> {
  return fetch(`/api/users/${userId}/public`)
    .then(r => r.json());
}

// Component showing only public info
function UserCard({ user }: { user: UserPublicInfo }) {
  return (
    

{user.name}

{user.email}

{/* No access to password or address - they don't exist! */}
); }

🎯 Omit<T, K> - Remove Specific Properties

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// Omit sensitive properties
type UserWithoutPassword = Omit<User, 'password'>;
// Equivalent to:
// {
//   id: number;
//   name: string;
//   email: string;
// }

// Practical use: Form inputs (exclude auto-generated fields)
type UserFormData = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;

interface UserFormProps {
  initialValues?: Partial<UserFormData>;
  onSubmit: (data: UserFormData) => void;
}

// Omit for extending component props
interface ButtonProps {
  variant: 'primary' | 'secondary';
  size: 'small' | 'medium' | 'large';
  onClick: () => void;
}

// Create LinkButton that uses 'href' instead of 'onClick'
type LinkButtonProps = Omit<ButtonProps, 'onClick'> & {
  href: string;
};

function LinkButton({ href, variant, size }: LinkButtonProps) {
  return (
    
      {/* Button content */}
    
  );
}

🎯 Record<K, T> - Create Object Type

// Create an object type with specific keys and value type
type UserRole = 'admin' | 'editor' | 'viewer';

type RolePermissions = Record<UserRole, string[]>;
// Equivalent to:
// {
//   admin: string[];
//   editor: string[];
//   viewer: string[];
// }

const permissions: RolePermissions = {
  admin: ['read', 'write', 'delete', 'manage'],
  editor: ['read', 'write'],
  viewer: ['read']
};

// Practical use: State by ID
type EntityById<T> = Record<string, T>;

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

const products: EntityById<Product> = {
  'prod-1': { id: 'prod-1', name: 'Widget', price: 10 },
  'prod-2': { id: 'prod-2', name: 'Gadget', price: 20 }
};

// Component displaying permissions
function PermissionsTable() {
  return (
    
        {Object.entries(permissions).map(([role, perms]) => (
          
        ))}
      
Role Permissions
{role} {perms.join(', ')}
); }

🎯 Extract<T, U> and Exclude<T, U> - Union Operations

type Status = 'idle' | 'loading' | 'success' | 'error';

// Extract only specific types from union
type SuccessStatus = Extract<Status, 'success'>; // 'success'
type ActiveStatus = Extract<Status, 'loading' | 'success'>; // 'loading' | 'success'

// Exclude specific types from union
type NonErrorStatus = Exclude<Status, 'error'>; // 'idle' | 'loading' | 'success'

// Practical use: Component with restricted props
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';

// Only allow safe variants for certain contexts
type SafeButtonVariant = Exclude<ButtonVariant, 'danger'>;

interface SafeButtonProps {
  variant: SafeButtonVariant;
  onClick: () => void;
}

function SafeButton({ variant, onClick }: SafeButtonProps) {
  return (
    
  );
}

// Usage
 // βœ… OK
 // ❌ Error

βœ… Utility Types Best Practices

  • Use Partial for optional updates and form data
  • Use Omit when extending component props
  • Use Pick for API response types
  • Use Record for lookup tables and dictionaries
  • Combine utility types: Readonly<Partial<User>>
  • Create type aliases for reusable combinations

🎯 Combining Utility Types

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  age: number;
}

// Combine multiple utility types
type UserUpdatePayload = Partial<Omit<User, 'id'>>;
// Allows updating any field except 'id', all fields optional

type ReadonlyUser = Readonly<User>;
// All fields readonly

type PublicUserInfo = Readonly<Pick<User, 'id' | 'name'>>;
// Only id and name, both readonly

// Real-world example
interface ApiResponse<T> {
  data: T;
  status: number;
  timestamp: number;
}

type UserResponse = ApiResponse<Readonly<Omit<User, 'password'>>>;
// User data without password, made readonly

πŸ”€ Conditional Types

Conditional types allow you to create types that depend on a conditionβ€”like an if statement for types. They enable incredibly powerful type transformations and are the foundation of many advanced TypeScript patterns.

πŸ“š Conditional Type Syntax

The syntax is: T extends U ? X : Y

// Basic conditional type
type IsString<T> = T extends string ? 'yes' : 'no';

type A = IsString<string>;  // 'yes'
type B = IsString<number>;  // 'no'
type C = IsString<boolean>; // 'no'

// Conditional type that returns different types
type ArrayOrSingle<T> = T extends any[] ? T[number] : T;

type D = ArrayOrSingle<string[]>;  // string
type E = ArrayOrSingle<number>;    // number
type F = ArrayOrSingle<boolean[]>; // boolean

πŸ“– How Conditional Types Work

Think of conditional types as:

  • T extends U - "Can T be assigned to U?"
  • ? X - "If yes, the type is X"
  • : Y - "If no, the type is Y"

🎯 Practical Example: Function Overload Replacement

// Without conditional types - need overloads
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): string;
function process(value: string | number | boolean): string | number {
  if (typeof value === 'string') return value.toUpperCase();
  if (typeof value === 'number') return value * 2;
  return value ? 'true' : 'false';
}

// βœ… With conditional types - single signature
type ProcessReturnType<T> = 
  T extends string ? string :
  T extends number ? number :
  T extends boolean ? string :
  never;

function processWithConditional<T extends string | number | boolean>(
  value: T
): ProcessReturnType<T> {
  if (typeof value === 'string') {
    return value.toUpperCase() as ProcessReturnType<T>;
  }
  if (typeof value === 'number') {
    return (value * 2) as ProcessReturnType<T>;
  }
  return (value ? 'true' : 'false') as ProcessReturnType<T>;
}

// Usage - return type is inferred correctly!
const str = processWithConditional('hello');  // string
const num = processWithConditional(42);       // number
const bool = processWithConditional(true);    // string

🎯 Extracting Types from Promises

// Unwrap Promise type
type Awaited<T> = T extends Promise<infer U> ? U : T;

type A = Awaited<Promise<string>>;  // string
type B = Awaited<Promise<number>>;  // number
type C = Awaited<string>;           // string (not a Promise)

// Practical use: Typing async function results
async function fetchUser(): Promise<User> {
  const response = await fetch('/api/user');
  return response.json();
}

type UserType = Awaited<ReturnType<typeof fetchUser>>; // User

// React hook with conditional return type
type UseAsyncState<T> = {
  data: Awaited<T> | null;
  loading: boolean;
  error: Error | null;
};

function useAsync<T extends Promise<any>>(
  asyncFn: () => T
): UseAsyncState<T> {
  const [data, setData] = useState<Awaited<T> | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    asyncFn()
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);
  
  return { data, loading, error };
}

🎯 Inferring Types with 'infer' Keyword

The infer keyword lets you extract and name types from within conditional types:

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

function getUser() {
  return { id: 1, name: 'John' };
}

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

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

function createUser(name: string, email: string, age: number) {
  return { name, email, age };
}

type CreateUserParams = Parameters<typeof createUser>; 
// [name: string, email: string, age: number]

// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : T;

type NumberArray = ElementType<number[]>;  // number
type StringArray = ElementType<string[]>;  // string
type NotArray = ElementType<boolean>;      // boolean

// Practical example: Extract component props
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;

function Button({ label, onClick }: { label: string; onClick: () => void }) {
  return <button onClick={onClick}>{label}</button>;
}

type ButtonProps = ComponentProps<typeof Button>;
// { label: string; onClick: () => void; }

πŸ’‘ The 'infer' Keyword

infer introduces a type variable that TypeScript will infer from the structure. Think of it as "figure out what this type is and call it R (or P, or E, etc.)."

🎯 Distributive Conditional Types

When a conditional type is applied to a union type, it distributes over each member:

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

type A = ToArray<string | number>;
// Distributes to: ToArray<string> | ToArray<number>
// Results in: string[] | number[]

// Non-distributive (wrapped in tuple)
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type B = ToArrayNonDist<string | number>;
// Results in: (string | number)[]

// Practical example: Filter nullable types
type NonNullable<T> = T extends null | undefined ? never : T;

type C = NonNullable<string | null | number | undefined>;
// Distributes and filters: string | number

// React component example
type EventHandler<T> = T extends keyof HTMLElementEventMap
  ? (event: HTMLElementEventMap[T]) => void
  : never;

type ClickHandler = EventHandler<'click'>; 
// (event: MouseEvent) => void

type KeyHandler = EventHandler<'keydown'>; 
// (event: KeyboardEvent) => void

🎯 Advanced Pattern: Recursive Conditional Types

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

interface NestedData {
  user: {
    name: string;
    address: {
      street: string;
      city: string;
    };
  };
}

type ImmutableData = DeepReadonly<NestedData>;
// All nested properties are readonly!

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

type PartialUpdate = DeepPartial<NestedData>;
// All nested properties are optional!

// Practical use in React
interface FormState {
  user: {
    name: string;
    email: string;
    profile: {
      bio: string;
      avatar: string;
    };
  };
}

function updateForm(updates: DeepPartial<FormState>) {
  // Can update any nested property
  // updateForm({ user: { profile: { bio: 'New bio' } } })
}

βœ… Conditional Types Best Practices

  • Use conditional types to reduce code duplication
  • Combine with infer to extract nested types
  • Leverage distributive behavior for union types
  • Create reusable utility types for your domain
  • Document complex conditional types with examples

πŸ“ Template Literal Types

Template literal types build on JavaScript's template literal strings to create precise string types. They're perfect for type-safe string manipulation, CSS classes, event names, and more.

πŸ“š Basic Template Literal Types

// Basic template literal type
type Greeting = `hello ${string}`;

const g1: Greeting = 'hello world';     // βœ… OK
const g2: Greeting = 'hello there';     // βœ… OK
const g3: Greeting = 'goodbye world';   // ❌ Error

// Combining literal types
type Color = 'red' | 'blue' | 'green';
type Size = 'small' | 'medium' | 'large';

type ColoredSize = `${Color}-${Size}`;
// Results in:
// 'red-small' | 'red-medium' | 'red-large' |
// 'blue-small' | 'blue-medium' | 'blue-large' |
// 'green-small' | 'green-medium' | 'green-large'

// React component using template literals
interface ButtonProps {
  variant: ColoredSize;
  onClick: () => void;
}

function Button({ variant, onClick }: ButtonProps) {
  return (
    <button className={`btn-${variant}`} onClick={onClick}>
      Click me
    </button>
  );
}

// Usage - fully type-safe!
<Button variant="blue-large" onClick={handler} /> // βœ… OK
<Button variant="purple-huge" onClick={handler} /> // ❌ Error

πŸ“– Template Literal Type Features

  • Combine multiple string literal types
  • Create thousands of precise string types automatically
  • Work with mapped types for key transformation
  • Provide autocomplete for string values

🎯 CSS Class Names

// Type-safe CSS class generation
type Spacing = 0 | 1 | 2 | 3 | 4 | 5;
type SpacingProperty = 'margin' | 'padding';
type Direction = 'top' | 'right' | 'bottom' | 'left';

type SpacingClass = `${SpacingProperty}-${Direction}-${Spacing}`;
// 'margin-top-0' | 'margin-top-1' | ... | 'padding-left-5'

interface BoxProps {
  className: SpacingClass;
  children: React.ReactNode;
}

function Box({ className, children }: BoxProps) {
  return <div className={className}>{children}</div>;
}

// Usage
<Box className="margin-top-3">Content</Box> // βœ… OK
<Box className="margin-center-3">Content</Box> // ❌ Error

// Responsive design classes
type Breakpoint = 'sm' | 'md' | 'lg' | 'xl';
type Display = 'block' | 'flex' | 'grid' | 'none';

type ResponsiveDisplay = `${Breakpoint}:${Display}`;
// 'sm:block' | 'sm:flex' | ... | 'xl:none'

type TailwindClass = ResponsiveDisplay | SpacingClass;

interface ResponsiveBoxProps {
  className: TailwindClass;
  children: React.ReactNode;
}

function ResponsiveBox({ className, children }: ResponsiveBoxProps) {
  return <div className={className}>{children}</div>;
}

🎯 Event Handler Type Safety

// Type-safe event handlers
type EventName = 'click' | 'focus' | 'blur' | 'change' | 'submit';
type HandlerName<T extends string> = `on${Capitalize<T>}`;

type ClickHandler = HandlerName<'click'>;   // 'onClick'
type FocusHandler = HandlerName<'focus'>;   // 'onFocus'
type ChangeHandler = HandlerName<'change'>; // 'onChange'

// Generate all handler names
type AllHandlers = HandlerName<EventName>;
// 'onClick' | 'onFocus' | 'onBlur' | 'onChange' | 'onSubmit'

// Generic event emitter with type-safe events
type EventMap = {
  'user:login': { userId: number; timestamp: Date };
  'user:logout': { userId: number };
  'data:update': { id: string; data: unknown };
};

class TypedEventEmitter<T extends Record<string, any>> {
  private listeners: Partial<{
    [K in keyof T]: Array<(data: T[K]) => void>
  }> = {};

  on<K extends keyof T>(event: K, handler: (data: T[K]) => void) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(handler);
  }

  emit<K extends keyof T>(event: K, data: T[K]) {
    this.listeners[event]?.forEach(handler => handler(data));
  }
}

// Usage - fully typed!
const emitter = new TypedEventEmitter<EventMap>();

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

emitter.emit('user:login', { 
  userId: 123, 
  timestamp: new Date() 
}); // βœ… OK

emitter.emit('user:login', { 
  userId: 123 
}); // ❌ Error - missing timestamp

🎯 API Route Type Safety

// Type-safe API routes
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ApiVersion = 'v1' | 'v2';

type ApiRoute = `/api/${ApiVersion}/${string}`;

type UserRoute = `/api/${ApiVersion}/users${'' | `/${number}`}`;
// '/api/v1/users' | '/api/v1/users/123' | '/api/v2/users' | ...

interface ApiClient {
  get(url: UserRoute): Promise<User | User[]>;
  post(url: `/api/${ApiVersion}/users`, data: Partial<User>): Promise<User>;
  put(url: `/api/${ApiVersion}/users/${number}`, data: User): Promise<User>;
  delete(url: `/api/${ApiVersion}/users/${number}`): Promise<void>;
}

// Usage
const client: ApiClient = {
  async get(url) { /* implementation */ },
  async post(url, data) { /* implementation */ },
  async put(url, data) { /* implementation */ },
  async delete(url) { /* implementation */ }
};

// Type-safe API calls
await client.get('/api/v1/users');      // βœ… OK
await client.get('/api/v1/users/123');  // βœ… OK
await client.get('/api/v1/products');   // ❌ Error
await client.post('/api/v1/users', { name: 'John' }); // βœ… OK

🎯 Mapped Types with Template Literals

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

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

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

type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getEmail: () => string;
// }

type UserSetters = Setters<User>;
// {
//   setName: (value: string) => void;
//   setAge: (value: number) => void;
//   setEmail: (value: string) => void;
// }

type UserWithAccessors = User & UserGetters & UserSetters;

// Implementation
class UserModel implements UserWithAccessors {
  name: string = '';
  age: number = 0;
  email: string = '';

  getName = () => this.name;
  getAge = () => this.age;
  getEmail = () => this.email;

  setName = (value: string) => { this.name = value; };
  setAge = (value: number) => { this.age = value; };
  setEmail = (value: string) => { this.email = value; };
}

// React hook using template literals
type StateUpdater<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

type UseStateReturn<T> = T & StateUpdater<T>;

function useTypedState<T extends Record<string, any>>(
  initialState: T
): UseStateReturn<T> {
  const [state, setState] = useState(initialState);

  const updaters = Object.keys(initialState).reduce((acc, key) => {
    const setterName = `set${key.charAt(0).toUpperCase() + key.slice(1)}`;
    acc[setterName] = (value: any) => {
      setState(prev => ({ ...prev, [key]: value }));
    };
    return acc;
  }, {} as any);

  return { ...state, ...updaters };
}

// Usage
function UserForm() {
  const { name, email, setName, setEmail } = useTypedState({
    name: '',
    email: ''
  });

  return (
    <form>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
    </form>
  );
}
graph LR A[String Literal Types] --> B[Template Literal] B --> C[Combine Types] C --> D[Generate All Combinations] D --> E[Type-Safe Strings] E --> F[CSS Classes] E --> G[Event Names] E --> H[API Routes] E --> I[Method Names] style A fill:#667eea,color:#fff style E fill:#c8e6c9 style F fill:#e8f5e9 style G fill:#e8f5e9 style H fill:#e8f5e9 style I fill:#e8f5e9

βœ… Template Literal Types Best Practices

  • Use for type-safe string constants (routes, class names, event names)
  • Combine with mapped types for powerful transformations
  • Keep combinations reasonable (avoid type explosion)
  • Document the pattern when types become complex
  • Consider performance with very large unions

πŸ‹οΈ Practice Exercises

Exercise 1: Generic Data Table

Objective: Create a fully generic, type-safe data table component

Task: Build a DataTable component that:

  • Works with any data type
  • Ensures column keys match data properties
  • Provides type-safe sorting and filtering
  • Has customizable cell renderers
πŸ’‘ Hints
  • Use generics for the data type: <T>
  • Use keyof T for column keys
  • Use T[K] to get the type of specific properties
  • Consider using Record<keyof T, (value: T[keyof T]) => React.ReactNode> for custom renderers
βœ… Solution Approach
interface Column<T> {
  key: keyof T;
  label: string;
  render?: (value: T[keyof T], item: T) => React.ReactNode;
  sortable?: boolean;
}

interface DataTableProps<T> {
  data: T[];
  columns: Column<T>[];
  keyExtractor: (item: T) => string | number;
  onRowClick?: (item: T) => void;
}

function DataTable<T>({ 
  data, 
  columns, 
  keyExtractor,
  onRowClick 
}: DataTableProps<T>) {
  const [sortKey, setSortKey] = useState<keyof T | null>(null);
  const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
  
  const sortedData = useMemo(() => {
    if (!sortKey) return data;
    
    return [...data].sort((a, b) => {
      const aVal = a[sortKey];
      const bVal = b[sortKey];
      
      if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
      if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
      return 0;
    });
  }, [data, sortKey, sortDirection]);
  
  return (
    <table>
      <thead>
        <tr>
          {columns.map(col => (
            <th key={String(col.key)}>
              {col.label}
              {col.sortable && (
                <button onClick={() => setSortKey(col.key)}>
                  Sort
                </button>
              )}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {sortedData.map(item => (
          <tr 
            key={keyExtractor(item)}
            onClick={() => onRowClick?.(item)}
          >
            {columns.map(col => (
              <td key={String(col.key)}>
                {col.render 
                  ? col.render(item[col.key], item)
                  : String(item[col.key])
                }
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Exercise 2: Type-Safe State Machine

Objective: Implement a state machine using discriminated unions

Task: Create a traffic light state machine with:

  • States: Red, Yellow, Green
  • Type-safe transitions
  • Automatic timing
  • Prevent invalid state transitions
πŸ’‘ Hints
  • Use discriminated unions for states
  • Each state should know its next state
  • Use useReducer for state management
  • Include timing information in state
βœ… Solution Approach
type TrafficLightState =
  | { light: 'red'; duration: 5000 }
  | { light: 'yellow'; duration: 2000 }
  | { light: 'green'; duration: 5000 };

type TrafficLightAction =
  | { type: 'NEXT' }
  | { type: 'RESET' };

function trafficLightReducer(
  state: TrafficLightState,
  action: TrafficLightAction
): TrafficLightState {
  switch (action.type) {
    case 'NEXT':
      switch (state.light) {
        case 'red':
          return { light: 'green', duration: 5000 };
        case 'green':
          return { light: 'yellow', duration: 2000 };
        case 'yellow':
          return { light: 'red', duration: 5000 };
      }
    case 'RESET':
      return { light: 'red', duration: 5000 };
    default:
      const _exhaustive: never = action;
      return state;
  }
}

function TrafficLight() {
  const [state, dispatch] = useReducer(
    trafficLightReducer,
    { light: 'red', duration: 5000 }
  );
  
  useEffect(() => {
    const timer = setTimeout(() => {
      dispatch({ type: 'NEXT' });
    }, state.duration);
    
    return () => clearTimeout(timer);
  }, [state]);
  
  return (
    <div className={`traffic-light ${state.light}`}>
      <div className={state.light === 'red' ? 'active' : ''}>Red</div>
      <div className={state.light === 'yellow' ? 'active' : ''}>Yellow</div>
      <div className={state.light === 'green' ? 'active' : ''}>Green</div>
    </div>
  );
}

Exercise 3: Type-Safe Form Builder

Objective: Create a form builder with full type safety

Task: Build a form system where:

  • Field names are typed based on schema
  • Validation rules match field types
  • Form values are fully typed
  • Errors are associated with correct fields
πŸ’‘ Hints
  • Use generics to capture form schema
  • Use Record<keyof T, ...> for field-specific data
  • Use conditional types for validation rules
  • Consider using Partial for optional validation
βœ… Solution Approach
type ValidationRule<T> = (value: T) => string | undefined;

type FormSchema<T> = {
  [K in keyof T]: {
    type: 'text' | 'number' | 'email' | 'password';
    label: string;
    required?: boolean;
    validate?: ValidationRule<T[K]>;
  };
};

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

type FormErrors<T> = Partial<Record<keyof T, string>>;

interface UseFormReturn<T> {
  values: Partial<FormValues<T>>;
  errors: FormErrors<T>;
  setValue: <K extends keyof T>(field: K, value: T[K]) => void;
  handleSubmit: (onSubmit: (values: FormValues<T>) => void) => (e: React.FormEvent) => void;
}

function useForm<T extends Record<string, any>>(
  schema: FormSchema<T>
): UseFormReturn<T> {
  const [values, setValues] = useState<Partial<FormValues<T>>>({});
  const [errors, setErrors] = useState<FormErrors<T>>({});
  
  const setValue = <K extends keyof T>(field: K, value: T[K]) => {
    setValues(prev => ({ ...prev, [field]: value }));
    
    // Validate
    const fieldSchema = schema[field];
    if (fieldSchema.validate) {
      const error = fieldSchema.validate(value);
      setErrors(prev => ({ ...prev, [field]: error }));
    }
  };
  
  const handleSubmit = (onSubmit: (values: FormValues<T>) => void) => {
    return (e: React.FormEvent) => {
      e.preventDefault();
      
      // Validate all fields
      const newErrors: FormErrors<T> = {};
      let hasErrors = false;
      
      for (const key in schema) {
        const field = schema[key];
        const value = values[key];
        
        if (field.required && !value) {
          newErrors[key] = 'This field is required';
          hasErrors = true;
        } else if (field.validate && value) {
          const error = field.validate(value);
          if (error) {
            newErrors[key] = error;
            hasErrors = true;
          }
        }
      }
      
      setErrors(newErrors);
      
      if (!hasErrors) {
        onSubmit(values as FormValues<T>);
      }
    };
  };
  
  return { values, errors, setValue, handleSubmit };
}

🎯 Lesson Summary

Congratulations! You've mastered advanced TypeScript patterns that professional developers use to build robust, maintainable React applications. These patterns aren't just academic exercisesβ€”they're practical tools that will make your code safer and more expressive.

πŸ“š Key Takeaways

  • Generics: Create flexible, reusable components that work with any type
  • Discriminated Unions: Model complex state with impossible states ruled out
  • Type Guards: Safely narrow types at runtime
  • Utility Types: Transform types without duplication
  • Conditional Types: Create types that depend on conditions
  • Template Literals: Build precise string types for type-safe strings

πŸš€ Next Steps

Now that you understand advanced TypeScript patterns, you're ready to:

  • Learn accessibility best practices in Lesson 10.3
  • Master build and deployment in Lesson 10.4
  • Explore advanced topics in Lesson 10.5
  • Apply these patterns to build a robust component library
  • Refactor existing code with advanced types

⚠️ Remember

Advanced TypeScript patterns are powerful, but use them judiciously:

  • Prioritize readability over cleverness
  • Document complex types with examples
  • Start simple and add complexity only when needed
  • Consider your team's TypeScript experience
  • Balance type safety with development speed

βœ… Mastery Checklist

You've mastered advanced TypeScript when you can:

  • βœ… Create generic components that maintain full type safety
  • βœ… Model complex state machines with discriminated unions
  • βœ… Write custom type guards for domain types
  • βœ… Combine utility types for powerful transformations
  • βœ… Use conditional types to reduce code duplication
  • βœ… Build type-safe string constants with template literals
  • βœ… Explain when and why to use each pattern