📖 TypeScript Mastery - TypeScript trong Frontend Development
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>
  );
}

📝 Bài tập (1)

  1. Xây dựng component library với TypeScript strict types

Bài học "TypeScript trong Frontend Development" - Khóa học "TypeScript Mastery"