75 phút
TypeScript trong Frontend Development
React với TypeScript
Functional Components
import React from 'react';
interface UserCardProps {
user: {
id: number;
name: string;
email: string;
avatar?: string;
};
onEdit?: (user: User) => void;
onDelete?: (userId: number) => void;
}
const UserCard: React.FC<UserCardProps> = ({
user,
onEdit,
onDelete
}) => {
return (
<div className="user-card">
<img
src={user.avatar || '/default-avatar.png'}
alt={user.name}
/>
<h3>{user.name}</h3>
<p>{user.email}</p>
<div className="actions">
{onEdit && (
<button onClick={() => onEdit(user)}>Edit</button>
)}
{onDelete && (
<button onClick={() => onDelete(user.id)}>Delete</button>
)}
</div>
</div>
);
};
Hooks với TypeScript
import { useState, useEffect, useCallback } from 'react';
interface User {
id: number;
name: string;
email: string;
}
interface UseUsersResult {
users: User[];
loading: boolean;
error: string | null;
addUser: (user: Omit<User, 'id'>) => void;
removeUser: (userId: number) => void;
}
function useUsers(): UseUsersResult {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
const addUser = useCallback((userData: Omit<User, 'id'>) => {
const newUser: User = {
...userData,
id: Math.max(0, ...users.map(u => u.id)) + 1
};
setUsers(prev => [...prev, newUser]);
}, [users]);
const removeUser = useCallback((userId: number) => {
setUsers(prev => prev.filter(user => user.id !== userId));
}, []);
return { users, loading, error, addUser, removeUser };
}
Vue với TypeScript
Composition API
<template>
<div>
<h1>{{ title }}</h1>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }} - {{ user.email }}
</li>
</ul>
<button @click="addUser">Add User</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
interface User {
id: number;
name: string;
email: string;
}
// Reactive state
const users = ref<User[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
// Computed properties
const userCount = computed(() => users.value.length);
const title = computed(() => `Users (${userCount.value})`);
// Methods
const fetchUsers = async () => {
try {
loading.value = true;
const response = await fetch('/api/users');
users.value = await response.json();
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error';
} finally {
loading.value = false;
}
};
const addUser = () => {
const newUser: User = {
id: Math.max(0, ...users.value.map(u => u.id)) + 1,
name: 'New User',
email: 'new@example.com'
};
users.value.push(newUser);
};
// Lifecycle
onMounted(() => {
fetchUsers();
});
</script>
Props với TypeScript
<template>
<div class="user-list">
<user-card
v-for="user in filteredUsers"
:key="user.id"
:user="user"
@edit="handleEdit"
@delete="handleDelete"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
users: User[];
searchQuery?: string;
showInactive?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
searchQuery: '',
showInactive: false
});
const emit = defineEmits<{
edit: [user: User];
delete: [userId: number];
}>();
const filteredUsers = computed(() => {
return props.users.filter(user => {
const matchesSearch = user.name.toLowerCase()
.includes(props.searchQuery.toLowerCase());
const isActive = props.showInactive || user.active;
return matchesSearch && isActive;
});
});
const handleEdit = (user: User) => {
emit('edit', user);
};
const handleDelete = (userId: number) => {
emit('delete', userId);
};
</script>
State Management
Redux với TypeScript
// types.ts
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
interface UsersState {
items: User[];
loading: boolean;
error: string | null;
}
// actions.ts
const FETCH_USERS_REQUEST = 'users/FETCH_USERS_REQUEST';
const FETCH_USERS_SUCCESS = 'users/FETCH_USERS_SUCCESS';
const FETCH_USERS_FAILURE = 'users/FETCH_USERS_FAILURE';
interface FetchUsersRequest {
type: typeof FETCH_USERS_REQUEST;
}
interface FetchUsersSuccess {
type: typeof FETCH_USERS_SUCCESS;
payload: User[];
}
interface FetchUsersFailure {
type: typeof FETCH_USERS_FAILURE;
payload: string;
}
type UsersAction =
| FetchUsersRequest
| FetchUsersSuccess
| FetchUsersFailure;
// reducer.ts
const initialState: UsersState = {
items: [],
loading: false,
error: null
};
export function usersReducer(
state = initialState,
action: UsersAction
): UsersState {
switch (action.type) {
case FETCH_USERS_REQUEST:
return { ...state, loading: true, error: null };
case FETCH_USERS_SUCCESS:
return { ...state, loading: false, items: action.payload };
case FETCH_USERS_FAILURE:
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
Pinia với TypeScript
import { defineStore } from 'pinia';
interface User {
id: number;
name: string;
email: string;
}
interface UsersState {
users: User[];
loading: boolean;
error: string | null;
}
export const useUsersStore = defineStore('users', {
state: (): UsersState => ({
users: [],
loading: false,
error: null
}),
getters: {
activeUsers: (state) => state.users.filter(user => user.active),
userCount: (state) => state.users.length
},
actions: {
async fetchUsers() {
this.loading = true;
this.error = null;
try {
const response = await fetch('/api/users');
this.users = await response.json();
} catch (error) {
this.error = error instanceof Error ? error.message : 'Unknown error';
} finally {
this.loading = false;
}
},
addUser(userData: Omit<User, 'id'>) {
const newUser: User = {
...userData,
id: Math.max(0, ...this.users.map(u => u.id)) + 1
};
this.users.push(newUser);
},
removeUser(userId: number) {
this.users = this.users.filter(user => user.id !== userId);
}
}
});
Form Handling
Type-Safe Forms
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
interface FormErrors {
email?: string;
password?: string;
}
function validateLoginForm(form: LoginForm): FormErrors {
const errors: FormErrors = {};
if (!form.email) {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(form.email)) {
errors.email = 'Email is invalid';
}
if (!form.password) {
errors.password = 'Password is required';
} else if (form.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
return errors;
}
// React Hook Form với TypeScript
import { useForm } from 'react-hook-form';
type LoginFormData = {
email: string;
password: string;
rememberMe: boolean;
};
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<LoginFormData>();
const onSubmit = (data: LoginFormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /\S+@\S+\.\S+/,
message: 'Invalid email address'
}
})}
/>
{errors.email && <span>{errors.email.message}</span>}
<input
type="password"
{...register('password', {
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must be at least 8 characters'
}
})}
/>
{errors.password && <span>{errors.password.message}</span>}
<input type="checkbox" {...register('rememberMe')} />
<button type="submit">Login</button>
</form>
);
}