Lợi Dev

Giới thiệu

Tìm hiểu TypeScript, sự khác biệt với JavaScript và tại sao lại cần TypeScript ?

TypeScript là gì?

TypeScript (TS) là "bản mở rộng" của JavaScript, bổ sung hệ thống kiểu dữ liệu (static typing) và nhiều tính năng hỗ trợ lập trình hiện đại. Bạn viết TypeScript sẽ được tự động biên dịch (compile) về JavaScript để chạy trên trình duyệt hoặc Node.js.

Tóm gọn trong một câu

Bạn vẫn viết JavaScript, nhưng có thêm định nghĩa kiểu (types) và công cụ hỗ trợ mạnh hơn.

Ví dụ xuyên suốt bài này

Để dễ theo dõi, toàn bộ ví dụ trong bài sẽ xoay quanh một tình huống quen thuộc: xây dựng tính năng quản lý người dùng. Cùng một dữ liệu NguoiDung sẽ được dùng lại và mở rộng dần qua từng phần và xuyên suốt các trang tiếp theo trong docs này.


TypeScript khác JavaScript như thế nào?

1. Kiểu dữ liệu tĩnh (Static types)

  • JavaScript: kiểu dữ liệu chỉ được xác định khi code thực sự chạy, gọi là runtime. Bạn có thể gõ sai mà không ai báo, cho đến khi chạy thử và thấy lỗi.
  • TypeScript: bạn khai báo kiểu dữ liệu ngay khi viết code, lỗi được phát hiện trước khi chạy, gọi là compile time.
tinh-tuoi.js
function tinhTuoi(namSinh, namHienTai) {
  return namHienTai - namSinh;
}

tinhTuoi(2000, "2024"); // chạy được, nhưng ra "24" (ghép chuỗi) sai mà không ai báo

Nhờ vậy, bạn sửa lỗi ngay lúc gõ code, thay vì phải chạy thử rồi mới phát hiện ra.

2. Bắt lỗi sớm

TS phát hiện nhiều lỗi phổ biến ngay trong lúc viết code:

nguoi-dung.ts
type NguoiDung = {
  ten: string;
  tuoi: number;
};

const nguoiDung1: NguoiDung = { ten: "Loi", tuoi: 20 };

console.log(nguoiDung1.tuoii);
// ❌ Lỗi: Property 'tuoii' does not exist on type 'NguoiDung'. Did you mean 'tuoi'?

Trong JavaScript thuần

nguoiDung1.tuoii sẽ chỉ âm thầm trả về undefined mà không có cảnh báo gì cả.

3. Trải nghiệm IDE tốt hơn

Vì TS biết cấu trúc dữ liệu của bạn, các IDE như VS Code có thể:

  • Gợi ý code (autocomplete) chính xác, gõ nguoiDung1. là hiện ngay ten, tuoi thay vì đoán mò.
  • Nhảy tới định nghĩa (Go to Definition) chỉ bằng ctrl + click.
  • Đổi tên an toàn (Rename Symbol) đổi tên một field, TS tự cập nhật mọi nơi khác dùng field đó (F2 để đổi).

4. JavaScript trong TypeScript

Mọi đoạn code JavaScript hợp lệ cũng là code TypeScript hợp lệ. Bạn có thể đổi đuôi file từ .js sang .ts mà không cần sửa gì, rồi thêm kiểu dữ liệu dần dần, không cần viết lại toàn bộ dự án cùng lúc.


Lợi ích nổi bật của TypeScript

Gợi ý code chính xác

IDE hiểu cấu trúc dữ liệu và gợi ý đúng thuộc tính/hàm theo ngữ cảnh, giảm thời gian tra tài liệu và lỗi gõ sai.

Ít bug hơn

Bắt lỗi kiểu dữ liệu, giá trị undefined, hoặc thay đổi cấu trúc mà quên sửa chỗ gọi ngay khi viết code.

Dễ bảo trì & mở rộng

Types hoạt động như tài liệu sống, giúp người mới hiểu dữ liệu đi qua hệ thống ra sao.

Refactor an toàn

Đổi tên field, tách module, đổi tham số hàm, TS báo lỗi ở mọi nơi bị ảnh hưởng.

Hợp với teamwork

Type tạo ra 'hợp đồng' dữ liệu rõ ràng giữa frontend và backend.

Hỗ trợ JS hiện đại

Dùng cú pháp ES mới, TS tự biên dịch về target phù hợp với môi trường chạy.

Xử lý giá trị có thể undefined

Không phải người dùng nào cũng đã nạp tiền vào ví field soDuVi có thể không tồn tại:

nguoi-dung.ts
type NguoiDung = {
  ten: string;
  tuoi: number;
  soDuVi?: number; // dấu ? nghĩa là có thể không tồn tại
};

function hienThiSoDu(nd: NguoiDung) {
  console.log(nd.soDuVi.toFixed(2));
  // ❌ Lỗi: 'soDuVi' có thể là 'undefined'
  // vì nếu là 'undefined', không có giá trị thì làm gì có thể gọi tiếp .toFixed(2)
}

TS buộc bạn xử lý trường hợp này trước:

nguoi-dung.ts
function hienThiSoDu(nd: NguoiDung) {
  if (nd.soDuVi !== undefined) {
    console.log(nd.soDuVi.toFixed(2)); // ✅ An toàn
  }
}

// hoặc

function hienThiSoDu(nd: NguoiDung) {
  console.log(nd.soDuVi?.toFixed(2)); // ✅ An toàn
}

// Dấu ? nghĩa là nếu giá trị là undefined thì trả về undefined luôn, không truy cập tiếp

Type làm "hợp đồng" dữ liệu

Giả sử backend có một API trả về thông tin người dùng. Nếu không có gì mô tả trước hình dạng dữ liệu, frontend chỉ còn cách:

  • Gọi thử API rồi console.log ra xem response có field gì
  • Hoặc mở DevTools (tab Network) / dùng Postman để tự soi response
  • Hoặc phải hỏi lại backend dev xem field tên là gì, kiểu dữ liệu ra sao

Cách này tạm ổn lúc mới viết, nhưng vài tuần/tháng sau quay lại đọc code, hoặc một người mới vào dự án đọc code lần đầu sẽ không biết chắc dữ liệu trả về có field gì mà không lặp lại các bước trên. Mỗi lần cần nhớ lại cấu trúc dữ liệu là một lần phải rời khỏi code để đi tra cứu.

fetch('/api/nguoi-dung/1')
  .then(res => res.json())
  .then(data => {
    console.log(data); // phải chạy thử mới biết data có field gì
    console.log(data.ten); // gõ đúng tên field không? không có gì đảm bảo cả
  });

Với TypeScript, bạn định nghĩa trước hình dạng dữ liệu bằng type, coi như một "bản cam kết" giữa backend và frontend rằng dữ liệu trả về sẽ luôn có đúng các field này:

types.ts
type PhanHoiNguoiDung = {
  id: number;
  ten: string;
  email: string;
};

Giờ đây, chỉ cần đọc thẳng type ngay trong code là biết dữ liệu có field gì, kiểu gì, không cần gọi API thật, không cần mở DevTools hay Postman, cũng không cần hỏi lại ai:

api.ts
async function layNguoiDung(id: number): Promise<PhanHoiNguoiDung> {
  const res = await fetch(`/api/nguoi-dung/${id}`);
  return res.json();
}

Khi dùng lại kết quả này ở nơi khác trong code:

const nd = await layNguoiDung(1);
console.log(nd.tenn);
// ❌ Lỗi ngay: Property 'tenn' does not exist on type 'PhanHoiNguoiDung'.
//    (gõ sai chính tả 'ten')

Nhờ vậy, nếu gõ sai tên field, TypeScript báo lỗi ngay lúc viết code, thay vì để bạn phát hiện lúc chạy app, hoặc tệ hơn là không phát hiện ra và bug tới tay người dùng.

Về ví dụ dùng fetch ở trên

Ví dụ trên chỉ mang tính minh họa cho cách type mô tả dữ liệu, không phải cách gọi API được khuyên dùng trong dự án thực tế. fetch thuần có một số hạn chế: phải tự parse JSON, tự xử lý lỗi HTTP (fetch không tự "throw" khi status là 404/500), không có sẵn cơ chế cache, retry, hay hủy request đang chạy.

Trong thực tế, các dự án thường dùng thư viện như Axios (gọi API tiện hơn, tự động parse JSON, xử lý lỗi rõ ràng hơn), hoặc kết hợp với TanStack Query (trước đây gọi là React Query) để quản lý cache, loading state, tự động gọi lại khi cần, đặc biệt hữu ích khi dùng chung với React. Type dữ liệu (PhanHoiNguoiDung ở trên) vẫn áp dụng được y hệt dù bạn dùng fetch, Axios, hay TanStack Query.

Promise hoạt động như thế nào?

fetch(...) không trả về dữ liệu ngay lập tức, vì gọi API cần thời gian (chờ mạng, chờ server xử lý). Thay vào đó, nó trả về một Promise: một "lời hứa" rằng dữ liệu sẽ có trong tương lai, ở một trong hai trạng thái:

  • Thành công (resolved): có dữ liệu trả về, code tiếp tục chạy với dữ liệu đó
  • Thất bại (rejected): có lỗi xảy ra (mất mạng, server lỗi...), cần được bắt (catch) lại

Từ khóa await giúp code "chờ" Promise hoàn tất rồi mới chạy tiếp dòng sau, viết theo trình tự từ trên xuống dễ đọc hơn thay vì lồng nhiều .then(). Đây là lý do hàm layNguoiDung ở trên khai báo kiểu trả về là Promise<PhanHoiNguoiDung>, báo trước rằng: "hàm này không trả PhanHoiNguoiDung ngay, mà trả một Promise, và khi Promise đó hoàn tất thành công thì bên trong sẽ là PhanHoiNguoiDung".


Đọc hiểu lỗi TypeScript

Lỗi TypeScript thường dài và có thuật ngữ lạ, khiến người mới dễ hoảng và bỏ qua không đọc kỹ. Thử bóc tách lại lỗi đã gặp ở trên:

Property 'tenn' does not exist on type 'PhanHoiNguoiDung'.

Xác định "đối tượng gây lỗi"

Property 'tenn' TS đang nói về thuộc tính (property) tên là tenn. Đây chính là chỗ bạn gõ trong code, thường là vị trí con trỏ nhấp nháy đỏ.

Xác định "TS mong đợi gì"

does not exist on type 'PhanHoiNguoiDung' TS cho biết thuộc tính đó không tồn tại trên type PhanHoiNguoiDung. Đây là type bạn (hoặc ai đó) đã khai báo trước bạn có thể ctrl + click vào tên type để nhảy tới xem nó có những field gì.

So sánh 2 phía

Một bên là thứ bạn gõ (tenn), một bên là thứ type cho phép (id, ten, email). Phần lớn lỗi TS đều theo mô-típ này: có 1 kiểu dữ liệu "chuẩn" đã khai báo, và 1 chỗ trong code đang không khớp với chuẩn đó do gõ sai, thiếu field, hoặc sai kiểu.

Mẹo đọc lỗi TS nhanh hơn

Với lỗi dài, đừng cố đọc hết một lượt. Đọc phần đầu câu trước (loại lỗi gì sai kiểu? thiếu field? không tồn tại?), sau đó tìm tên type/biến được nhắc tới và nhấp vào để xem định nghĩa gốc của nó. Đa số trường hợp, lỗi nằm ở việc gõ sai tên hoặc quên xử lý trường hợp undefined.


Giới hạn của TypeScript

TypeScript không kiểm tra được dữ liệu lúc runtime

TypeScript chỉ kiểm tra kiểu ở compile-time (lúc viết code), không chạy kèm theo lúc ứng dụng thực sự hoạt động. Nếu API trả về dữ liệu sai khác so với type đã khai báo (ví dụ PhanHoiNguoiDung khai báo id: number, nhưng backend lỡ trả về id: "1" dạng chuỗi), TypeScript vẫn tin tưởng type đó là đúng và không báo lỗi gì vì kiểm tra đã kết thúc từ lúc biên dịch.

Nói cách khác: TS đảm bảo code của bạn nhất quán với nhau, nhưng không đảm bảo dữ liệu thực tế từ bên ngoài (API, form người dùng nhập, file JSON...) đúng như bạn khai báo. Để kiểm tra dữ liệu thực tế lúc runtime, các dự án thường dùng thêm thư viện validate như Zod bên cạnh TypeScript.


Khi nào nên dùng TypeScript?

  • Dự án vừa/lớn, nhiều người cùng làm
  • Code cần bảo trì lâu dài
  • Bạn muốn refactor thường xuyên mà ít rủi ro
  • Dùng framework phổ biến (React, Tanstack Start, Next.js, NestJS, Angular…) hệ sinh thái TS rất mạnh

Tài nguyên tham khảo

On this page