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:
- User accounts with different roles
- User profiles with optional information
- User activity tracking
- 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;
}
Task 2.2: Create Social Links Type
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:
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(),
};
}
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,
};
}
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:
- Type Aliases: Creating named types for unions (
UserRole,AccountStatus) - Interfaces: Defining object shapes (
User,Address,SocialLinks) - Interface Extension: Building on existing interfaces (
UserProfile extends User) - Optional Properties: Handling missing data (
bio?,phone?) - Readonly Properties: Protecting immutable fields (
readonly id) - Discriminated Unions: Type-safe API responses (
ApiResponse<T>) - Utility Types: Using
PartialandOmitfor updates - 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
- Start with simple types: Build up from basic type aliases to complex interfaces
- Use unions for fixed sets: Role, status, and activity types are perfect for unions
- Extend interfaces: Do not repeat yourself - build on existing definitions
- Optional properties model reality: Not all data is always available
- Readonly protects data integrity: Use for IDs, timestamps, and other immutable fields
- Discriminated unions for variants: Perfect for API responses with different shapes
- 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.