From Zero to AI

Lesson 3.7: Practice - Typing User Data

Duration: 70 minutes

Overview

In this lesson, you will apply everything you learned in Module 3 to build a comprehensive user management system. This hands-on practice will reinforce your understanding of basic types, interfaces, type aliases, unions, and optional/readonly properties.


The Challenge

You are building a user management system for an application. You need to define types for:

  1. User accounts with different roles
  2. User profiles with optional information
  3. User activity tracking
  4. API responses for user operations

Let us build this step by step.


Part 1: Basic User Types

Task 1.1: Define User Roles

Create a type alias for user roles. Your system has three roles: admin, moderator, and user.

// Define the UserRole type
Solution
type UserRole = 'admin' | 'moderator' | 'user';

Task 1.2: Define Account Status

Create a type for account status: active, suspended, or deactivated.

// Define the AccountStatus type
Solution
type AccountStatus = 'active' | 'suspended' | 'deactivated';

Task 1.3: Create the Base User Interface

Create an interface for a user with:

  • Readonly id (string)
  • Username (string)
  • Email (string)
  • Role (using your UserRole type)
  • Status (using your AccountStatus type)
  • Readonly createdAt (Date)
// Define the User interface
Solution
interface User {
  readonly id: string;
  username: string;
  email: string;
  role: UserRole;
  status: AccountStatus;
  readonly createdAt: Date;
}

Part 2: User Profile

Task 2.1: Create Address Type

Create an interface for an address with street, city, state (optional), zipCode, and country.

// Define the Address interface
Solution
interface Address {
  street: string;
  city: string;
  state?: string;
  zipCode: string;
  country: string;
}

Create an interface for social media links where all properties are optional: twitter, github, linkedin, website.

// Define the SocialLinks interface
Solution
interface SocialLinks {
  twitter?: string;
  github?: string;
  linkedin?: string;
  website?: string;
}

Task 2.3: Create User Profile Interface

Create a UserProfile interface that extends User and adds:

  • Display name (optional)
  • Bio (optional, max 500 chars - just use string for now)
  • Avatar URL (optional)
  • Phone (optional)
  • Address (optional, using your Address type)
  • Social links (optional, using your SocialLinks type)
  • Date of birth (optional)
// Define the UserProfile interface
Solution
interface UserProfile extends User {
  displayName?: string;
  bio?: string;
  avatarUrl?: string;
  phone?: string;
  address?: Address;
  socialLinks?: SocialLinks;
  dateOfBirth?: Date;
}

Part 3: User Activity

Task 3.1: Define Activity Types

Create a type for different activity types: login, logout, profile_update, password_change, purchase.

// Define the ActivityType type
Solution
type ActivityType = 'login' | 'logout' | 'profile_update' | 'password_change' | 'purchase';

Task 3.2: Create Activity Record Interface

Create an interface for activity records with:

  • Readonly id (string)
  • User ID (string)
  • Type (using ActivityType)
  • Timestamp (Date)
  • IP address (optional)
  • User agent (optional)
  • Details (optional, object with string keys and unknown values)
// Define the ActivityRecord interface
Solution
interface ActivityRecord {
  readonly id: string;
  userId: string;
  type: ActivityType;
  timestamp: Date;
  ipAddress?: string;
  userAgent?: string;
  details?: Record<string, unknown>;
}

Part 4: API Responses

Task 4.1: Create API Response Types

Create a generic API response structure with:

  • Success response: has success (true), data (generic type T), and optional message
  • Error response: has success (false), error object with code (number) and message (string)

Use a discriminated union.

// Define the ApiResponse type
Solution
type ApiSuccess<T> = {
  success: true;
  data: T;
  message?: string;
};

type ApiError = {
  success: false;
  error: {
    code: number;
    message: string;
  };
};

type ApiResponse<T> = ApiSuccess<T> | ApiError;

Task 4.2: Create Specific Response Types

Using your ApiResponse type, create types for:

  • Single user response
  • User list response
  • User profile response
// Define the response types
Solution
type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<User[]>;
type UserProfileResponse = ApiResponse<UserProfile>;

Part 5: Putting It All Together

Task 5.1: Create User Functions

Implement these functions with proper types:

  1. createUser: Takes username, email, and optional role (defaults to "user"). Returns a new User.
// Implement createUser
Solution
function createUser(username: string, email: string, role: UserRole = 'user'): User {
  return {
    id: `user_${Date.now()}`,
    username,
    email,
    role,
    status: 'active',
    createdAt: new Date(),
  };
}
  1. updateUserProfile: Takes a UserProfile and partial updates (excluding id and createdAt). Returns updated UserProfile.
// Implement updateUserProfile
Solution
function updateUserProfile(
  profile: UserProfile,
  updates: Partial<Omit<UserProfile, 'id' | 'createdAt'>>
): UserProfile {
  return {
    ...profile,
    ...updates,
  };
}
  1. logActivity: Takes userId, activity type, and optional details. Returns an ActivityRecord.
// Implement logActivity
Solution
function logActivity(
  userId: string,
  type: ActivityType,
  details?: Record<string, unknown>
): ActivityRecord {
  return {
    id: `activity_${Date.now()}`,
    userId,
    type,
    timestamp: new Date(),
    details,
  };
}

Task 5.2: Create Response Handlers

Implement a function that handles API responses and extracts user data safely:

// Implement handleUserResponse
Solution
function handleUserResponse(response: UserResponse): User | null {
  if (response.success) {
    console.log(response.message ?? 'User fetched successfully');
    return response.data;
  } else {
    console.error(`Error ${response.error.code}: ${response.error.message}`);
    return null;
  }
}

Task 5.3: Create Display Functions

Implement a function that formats user profile information for display:

// Implement formatUserProfile
Solution
function formatUserProfile(profile: UserProfile): string {
  const lines: string[] = [];

  lines.push(`Username: ${profile.username}`);
  lines.push(`Email: ${profile.email}`);
  lines.push(`Role: ${profile.role}`);
  lines.push(`Status: ${profile.status}`);

  if (profile.displayName) {
    lines.push(`Display Name: ${profile.displayName}`);
  }

  if (profile.bio) {
    lines.push(`Bio: ${profile.bio}`);
  }

  if (profile.address) {
    const addr = profile.address;
    const addressParts = [addr.street, addr.city];
    if (addr.state) addressParts.push(addr.state);
    addressParts.push(addr.zipCode, addr.country);
    lines.push(`Address: ${addressParts.join(', ')}`);
  }

  if (profile.socialLinks) {
    const links = profile.socialLinks;
    if (links.twitter) lines.push(`Twitter: @${links.twitter}`);
    if (links.github) lines.push(`GitHub: ${links.github}`);
    if (links.linkedin) lines.push(`LinkedIn: ${links.linkedin}`);
    if (links.website) lines.push(`Website: ${links.website}`);
  }

  return lines.join('\n');
}

Complete Solution

Here is the complete code combining all parts:

// ============================================
// Type Definitions
// ============================================

// User roles and status
type UserRole = 'admin' | 'moderator' | 'user';
type AccountStatus = 'active' | 'suspended' | 'deactivated';

// Base user interface
interface User {
  readonly id: string;
  username: string;
  email: string;
  role: UserRole;
  status: AccountStatus;
  readonly createdAt: Date;
}

// Address
interface Address {
  street: string;
  city: string;
  state?: string;
  zipCode: string;
  country: string;
}

// Social links
interface SocialLinks {
  twitter?: string;
  github?: string;
  linkedin?: string;
  website?: string;
}

// Extended user profile
interface UserProfile extends User {
  displayName?: string;
  bio?: string;
  avatarUrl?: string;
  phone?: string;
  address?: Address;
  socialLinks?: SocialLinks;
  dateOfBirth?: Date;
}

// Activity tracking
type ActivityType = 'login' | 'logout' | 'profile_update' | 'password_change' | 'purchase';

interface ActivityRecord {
  readonly id: string;
  userId: string;
  type: ActivityType;
  timestamp: Date;
  ipAddress?: string;
  userAgent?: string;
  details?: Record<string, unknown>;
}

// API responses
type ApiSuccess<T> = {
  success: true;
  data: T;
  message?: string;
};

type ApiError = {
  success: false;
  error: {
    code: number;
    message: string;
  };
};

type ApiResponse<T> = ApiSuccess<T> | ApiError;
type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<User[]>;
type UserProfileResponse = ApiResponse<UserProfile>;

// ============================================
// Functions
// ============================================

function createUser(username: string, email: string, role: UserRole = 'user'): User {
  return {
    id: `user_${Date.now()}`,
    username,
    email,
    role,
    status: 'active',
    createdAt: new Date(),
  };
}

function updateUserProfile(
  profile: UserProfile,
  updates: Partial<Omit<UserProfile, 'id' | 'createdAt'>>
): UserProfile {
  return {
    ...profile,
    ...updates,
  };
}

function logActivity(
  userId: string,
  type: ActivityType,
  details?: Record<string, unknown>
): ActivityRecord {
  return {
    id: `activity_${Date.now()}`,
    userId,
    type,
    timestamp: new Date(),
    details,
  };
}

function handleUserResponse(response: UserResponse): User | null {
  if (response.success) {
    console.log(response.message ?? 'User fetched successfully');
    return response.data;
  } else {
    console.error(`Error ${response.error.code}: ${response.error.message}`);
    return null;
  }
}

function formatUserProfile(profile: UserProfile): string {
  const lines: string[] = [];

  lines.push(`Username: ${profile.username}`);
  lines.push(`Email: ${profile.email}`);
  lines.push(`Role: ${profile.role}`);
  lines.push(`Status: ${profile.status}`);

  if (profile.displayName) {
    lines.push(`Display Name: ${profile.displayName}`);
  }

  if (profile.bio) {
    lines.push(`Bio: ${profile.bio}`);
  }

  if (profile.address) {
    const addr = profile.address;
    const addressParts = [addr.street, addr.city];
    if (addr.state) addressParts.push(addr.state);
    addressParts.push(addr.zipCode, addr.country);
    lines.push(`Address: ${addressParts.join(', ')}`);
  }

  if (profile.socialLinks) {
    const links = profile.socialLinks;
    if (links.twitter) lines.push(`Twitter: @${links.twitter}`);
    if (links.github) lines.push(`GitHub: ${links.github}`);
    if (links.linkedin) lines.push(`LinkedIn: ${links.linkedin}`);
    if (links.website) lines.push(`Website: ${links.website}`);
  }

  return lines.join('\n');
}

// ============================================
// Usage Example
// ============================================

// Create a user
const alice = createUser('alice', 'alice@example.com', 'admin');
console.log('Created user:', alice);

// Extend to profile
let aliceProfile: UserProfile = {
  ...alice,
  displayName: 'Alice Smith',
  bio: 'TypeScript enthusiast and software developer',
  socialLinks: {
    twitter: 'alicedev',
    github: 'alice-dev',
  },
};

// Update profile
aliceProfile = updateUserProfile(aliceProfile, {
  address: {
    street: '123 Tech Lane',
    city: 'San Francisco',
    state: 'CA',
    zipCode: '94105',
    country: 'USA',
  },
});

// Log activity
const loginActivity = logActivity(alice.id, 'login', {
  browser: 'Chrome',
  os: 'macOS',
});
console.log('Activity logged:', loginActivity);

// Display profile
console.log('\nProfile:\n' + formatUserProfile(aliceProfile));

// Handle API response
const successResponse: UserResponse = {
  success: true,
  data: alice,
  message: 'User found',
};

const errorResponse: UserResponse = {
  success: false,
  error: {
    code: 404,
    message: 'User not found',
  },
};

const user1 = handleUserResponse(successResponse); // Returns alice
const user2 = handleUserResponse(errorResponse); // Returns null

Key Concepts Practiced

In this lesson, you practiced:

  1. Type Aliases: Creating named types for unions (UserRole, AccountStatus)
  2. Interfaces: Defining object shapes (User, Address, SocialLinks)
  3. Interface Extension: Building on existing interfaces (UserProfile extends User)
  4. Optional Properties: Handling missing data (bio?, phone?)
  5. Readonly Properties: Protecting immutable fields (readonly id)
  6. Discriminated Unions: Type-safe API responses (ApiResponse<T>)
  7. Utility Types: Using Partial and Omit for updates
  8. Type Narrowing: Checking success/failure in responses

Bonus Challenges

Try these additional challenges to deepen your understanding:

Challenge 1: Add Validation

Create a function that validates user input:

type ValidationResult = { valid: true } | { valid: false; errors: string[] };

function validateUserInput(input: Partial<User>): ValidationResult {
  // Implement validation logic
}

Challenge 2: Search and Filter

Create functions to search and filter users:

function findUsersByRole(users: User[], role: UserRole): User[] {
  // Implement search
}

function filterActiveUsers(users: User[]): User[] {
  // Implement filter
}

Challenge 3: Activity Analytics

Create types and functions for activity analytics:

interface ActivitySummary {
  userId: string;
  totalActivities: number;
  activitiesByType: Record<ActivityType, number>;
  lastActivity?: ActivityRecord;
}

function summarizeUserActivity(
  activities: readonly ActivityRecord[],
  userId: string
): ActivitySummary {
  // Implement summary
}

Key Takeaways

  1. Start with simple types: Build up from basic type aliases to complex interfaces
  2. Use unions for fixed sets: Role, status, and activity types are perfect for unions
  3. Extend interfaces: Do not repeat yourself - build on existing definitions
  4. Optional properties model reality: Not all data is always available
  5. Readonly protects data integrity: Use for IDs, timestamps, and other immutable fields
  6. Discriminated unions for variants: Perfect for API responses with different shapes
  7. Combine utility types: Partial<Omit<T, "id">> is a common pattern for updates

Resources

Resource Type Description
TypeScript Handbook: Everyday Types Documentation Comprehensive type reference
TypeScript Handbook: Object Types Documentation Interfaces and object types
TypeScript Playground Tool Test your solutions online

Module Complete

Congratulations! You have completed Module 3: Types and Interfaces. You now have a solid foundation in TypeScript's type system.

What you learned:

  • Basic types: string, number, boolean, arrays
  • Type inference and annotations
  • Union and literal types
  • Interfaces for object shapes
  • Type aliases for reusable types
  • Optional and readonly properties
  • Practical application in a user management system

Next up: Module 4 will cover Functions and Classes, where you will learn to type functions with parameters and return types, and create typed classes.

Continue to Module 4: Functions and Classes