📘 TypeScript with ReactJS All in One ⚛️
Truong Phung
Posted on November 12, 2024
Following is an example that helps us quickly review common TypeScript features like Enums
, Interfaces
, Generics
, Type Annotations
, Intersection Type
, Union Type
, Type Guard
....
1. TypeScript Page (TypeScriptPage.tsx
)
import React, { useState, useEffect, useReducer } from 'react';
import styles from './TypeScriptPage.module.css';
// Enum for user roles
enum UserRole {
Admin = 'Admin',
User = 'User',
Guest = 'Guest',
}
// Interface for base user data
interface User {
id: number;
name: string;
email?: string; // Optional property example
role: UserRole;
}
// AdminUser type using intersection to add permissions to User
type AdminUser = User & { permissions: string[] };
// Above code equivalent to following
// interface AdminUser extends User {
// permissions: string[]; // Specific to AdminUser
// }
// Union type for users with different possible structures
type UserType = User | AdminUser;
// Type Guard to check if a user is an AdminUser
const isAdminUser = (user: UserType): user is AdminUser => {
return (user as AdminUser).permissions !== undefined;
};
// Type alias for a list of users
type UserList = User[];
// Utility types examples
type UserWithoutEmail = Omit<User, 'email'>; // Omits the email property
type UserContactInfo = Pick<User, 'id' | 'name'>; // Picks only the id and name properties
type PartialUser = Partial<User>; // Makes all properties of User optional
// Generic function to fetch data, using generics for flexible return types with overloads and default parameter
async function fetchData<T>(url: string, options: RequestInit = {}): Promise<T> {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
// Action type for useReducer with different payloads
type UserAction =
| { type: 'add'; payload: User }
| { type: 'remove'; id: number }
| { type: 'update'; payload: { id: number; updates: PartialUser } };
// Reducer function for managing a list of users, demonstrating typed reducer usage
const userReducer = (state: User[], action: UserAction): User[] => {
switch (action.type) {
case 'add':
return [...state, action.payload];
case 'remove':
return state.filter(user => user.id !== action.id);
case 'update':
return state.map(user =>
user.id === action.payload.id ? { ...user, ...action.payload.updates } : user
);
default:
return state;
}
};
const TypeScriptPage: React.FC = () => {
const [users, dispatch] = useReducer(userReducer, []); // Using useReducer for state management
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [newUser, setNewUser] = useState<{ name: string; email: string; role: UserRole }>({
name: '',
email: '',
role: UserRole.Guest,
});
const [validationError, setValidationError] = useState<string | null>(null);
useEffect(() => {
fetchData<UserList>('https://jsonplaceholder.typicode.com/users')
.then(data => {
// Simulate user roles and create AdminUser type users
const usersWithRoles: UserType[] = data.map((user, index) =>
index % 2 === 0
? { ...user, role: UserRole.User }
: { ...user, role: UserRole.Admin, permissions: ['read', 'write'] }
);
usersWithRoles.forEach(user => dispatch({ type: 'add', payload: user })); // Adding users to the reducer
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) {
return <div className={styles.loading}>Loading...</div>;
}
if (error) {
return <div className={styles.error}>Error: {error}</div>;
}
// Example of Partial: updating only a subset of user properties
const updateUser = (id: number, updates: PartialUser) => {
dispatch({
type: 'update',
payload: { id, updates },
});
};
const handleAddUser = () => {
if (!newUser.name || !newUser.email) {
setValidationError('Name and email are required.');
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(newUser.email)) {
setValidationError('Please enter a valid email.');
return;
}
const newUserId = users.length ? Math.max(...users.map(user => user.id)) + 1 : 1;
const userToAdd: User = {
id: newUserId,
name: newUser.name,
email: newUser.email,
role: newUser.role,
};
dispatch({ type: 'add', payload: userToAdd });
setNewUser({ name: '', email: '', role: UserRole.Guest });
setValidationError(null);
};
return (
<div className={styles.container}>
<h1 className={styles.title}>TypeScript Advanced Features Page</h1>
{/* Demonstrate Omit utility */}
<div>
<h2 className={styles.subtitle}>Omit User Example</h2>
{/* Example: Email omitted from user */}
<code>{JSON.stringify({ id: 1, name: "User without email", role: UserRole.User } as UserWithoutEmail)}</code>
</div>
<div className={styles.addUserForm}>
<h2 className={styles.subtitle}>Add New User</h2>
<input
type="text"
placeholder="Name"
value={newUser.name}
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
className={styles.input}
/>
<input
type="email"
placeholder="Email"
value={newUser.email}
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
className={styles.input}
/>
<select
value={newUser.role}
onChange={(e) => setNewUser({ ...newUser, role: e.target.value as UserRole })}
className={styles.select}
>
<option value={UserRole.Guest}>Guest</option>
<option value={UserRole.User}>User</option>
<option value={UserRole.Admin}>Admin</option>
</select>
<button onClick={handleAddUser} className={styles.button}>
Add User
</button>
{validationError && <div className={styles.error}>{validationError}</div>}
</div>
<ul className={styles.userList}>
{users.map(user => (
<li key={user.id} className={styles.userItem}>
{user.name} ({user.email ?? 'N/A'}) - Role: {user.role}
{isAdminUser(user) && (
<div className={styles.permissions}>Permissions: {user.permissions.join(', ')}</div>
)}
{/* Example using Pick to show only selected properties */}
<div>
<strong>User Preview:</strong>{' '}
{JSON.stringify({ id: user.id, name: user.name } as UserContactInfo)}
</div>
{/* Update user role using Partial example */}
<button onClick={() => updateUser(user.id, { role: UserRole.Guest })} className={`${styles.button} ${styles.changeRoleButton}`}>
Change Role to Guest
</button>
<button onClick={() => dispatch({ type: 'remove', id: user.id })} className={`${styles.button} ${styles.removeUserButton}`}>
Remove User
</button>
</li>
))}
</ul>
</div>
);
};
export default TypeScriptPage;
2. Style (TypeScriptPage.module.css
)
Let's create a TypeScriptPage.module.css
file for styling and then apply these styles within TypeScriptPage
. Here’s how you can do it:
/* Main container for the page */
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
/* Title styling */
.title {
color: #2c3e50;
font-size: 2rem;
margin-bottom: 20px;
text-align: center;
}
/* Subtitle for section headings */
.subtitle {
color: #34495e;
font-size: 1.5rem;
margin-top: 20px;
margin-bottom: 10px;
}
/* User list container */
.userList {
list-style-type: none;
padding: 0;
margin-top: 20px;
}
/* Individual user item */
.userItem {
padding: 15px;
margin: 10px 0;
border: 1px solid #bdc3c7;
border-radius: 8px;
background-color: #ecf0f1;
}
/* Style for permissions for admin users */
.permissions {
color: #16a085;
font-weight: bold;
margin-top: 5px;
}
/* Button styling */
.button {
margin-right: 10px;
padding: 8px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
/* Button for changing role */
.changeRoleButton {
background-color: #3498db;
color: white;
}
.changeRoleButton:hover {
background-color: #2980b9;
}
/* Button for removing user */
.removeUserButton {
background-color: #e74c3c;
color: white;
}
.removeUserButton:hover {
background-color: #c0392b;
}
/* Loading and error messages */
.loading, .error {
text-align: center;
font-size: 1.2rem;
}
.loading {
color: #3498db;
}
.error {
color: #e74c3c;
}
/* Add User Form styling */
.addUserForm {
margin-top: 20px;
padding: 20px;
border: 1px solid #bdc3c7;
border-radius: 8px;
background-color: #f8f9fa;
}
.addUserForm input,
.addUserForm select {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #bdc3c7;
border-radius: 4px;
}
.addUserForm button {
background-color: #2ecc71;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.addUserForm button:hover {
background-color: #27ae60;
}
3. Summary
Here’s a breakdown of what’s included
- Enums: Demonstrates using an enum to define user roles.
-
Interfaces: Shows a clear example of defining an interface (
User
) for structured typing. -
Generics: The
fetchData<T>
function uses generics to make it reusable for various types. -
Type Annotations: Type annotations for state (
users
,loading
,error
) , component (React.FC
)... - Error Handling with Promises: Good use of error handling when fetching data.
- React with TypeScript: TypeScript integration in a React component.
-
Optional Properties: The
email
field inUser
is optional, shown withemail?: string
. -
Intersection Type (
AdminUser
): CombinesUser
with additional properties (permissions
) for admin-specific features. -
Union Type and Type Guard (
isAdminUser
):UserType
can be eitherUser
orAdminUser
, with a type guard to check if a user has admin permissions. -
Utility Types:
-
Omit
:UserWithoutEmail
excludesemail
fromUser
. -
Pick
:UserContactInfo
selects onlyid
andname
fromUser
. -
Partial
:PartialUser
makes allUser
properties optional, used in theupdateUser
function to apply partial updates.
-
-
useReducer
and Typed Actions:userReducer
manages theusers
state, allowing complex actions (add
,remove
,update
) with a strongly-typedUserAction
union. -
Type Assertions: In
updateUser
,payload
is cast toUser
to match theuserReducer
requirements after partial updates. -
Conditional Rendering with Optional Chaining:
Optional chaining is useful for handling deeply nested objects without worrying about
null
orundefined
. -
Add User Form: a form is added to input the new user's details. The
handleAddUser
function validates the input and dispatches an action to add the new user to the state. ThevalidationError
state is used to display any validation errors.
This example demonstrates common TypeScript functionality and give you a broad showcase of the language’s features.
If you found this helpful, let me know by leaving a 👍 or a comment!, or if you think this post could help someone, feel free to share it! Thank you very much! 😃
💖 💪 🙅 🚩
Truong Phung
Posted on November 12, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
webdev ⚡🚀 ReactJS, TypeScript, Vite with Redux and TanStack (React Query) In Practice ⚛️
November 13, 2024