← TypeScript Thực Chiến

Bài 2 · Cơ bản · 20 phút

Kiểu cơ bản & suy luận

Biên soạn bởi Nguyễn Anh Tuấn

Kiểu nguyên thuỷ, mảng, tuple, object và literal type; phân biệt any vs unknown vs never (vì sao tránh any, dùng unknown an toàn). Suy luận kiểu (type inference) để TS tự đoán thay vì chú thích mọi nơi, và as const để khoá literal.

Nhiều người tưởng dùng TypeScript là phải ghi kiểu cho mọi thứ. Thật ra ngược lại: TypeScript rất giỏi tự suy ra kiểu (type inference) từ giá trị bạn gán. Viết let tuoi = 5 là TS đã biết tuoi kiểu number rồi - thêm : number chỉ tổ thừa.

Đoán thử xem TypeScript suy ra kiểu gì cho mỗi khai báo, rồi bấm để kiểm:

Đoán xem TypeScript suy ra kiểu gì, rồi bấm để kiểm:

let tuoi = 5;
const ten = "Bo";
let ten = "Bo";
const diem = [1, 2, 3];
const meo = { ten: "Bo", tuoi: 2 };
const mau = ["do", "xanh"] as const;

Để ý hai dòng const ten = "Bo"let ten = "Bo": cùng giá trị mà TS suy ra khác kiểu. Đó gọi là widening (nới kiểu), ta sẽ mổ kỹ ở Bước 3.

  • TypeScript tự suy ra kiểu từ giá trị khởi tạo - không cần chú thích ở mọi nơi.
  • Chú thích kiểu chỉ cần ở chỗ TS không đoán được (tham số hàm, container rỗng).
  • Để TS suy luận giúp code gọn hơn mà vẫn an toàn y như chú thích tay.

Bộ kiểu cơ bản của TypeScript phủ lên đúng các kiểu giá trị JavaScript bạn đã quen:

cac-kieu-nen.ts

// Nguyen thuy
let ten: string = "Bo";
let tuoi: number = 2;
let deThuong: boolean = true;

// Mang: danh sach dai tuy y, cung kieu
let diem: number[] = [9, 8, 10];

// Tuple: do dai CO DINH, moi vi tri mot kieu
let cap: [string, number] = ["Bo", 2];

// Object: mo ta hinh dang
let meo: { ten: string; tuoi: number } = { ten: "Bo", tuoi: 2 };

Tuple không phải array

number[] là danh sách dài bao nhiêu cũng được, mọi phần tử cùng kiểu. [string, number]tuple: đúng 2 phần tử, vị trí đầu là chuỗi, vị trí sau là số. Dùng tuple khi thứ tự có ý nghĩa, vd trả về một cặp [tên, tuổi].
  • Nguyên thuỷ: string, number, boolean (và null, undefined).
  • Mảng number[] dài tuỳ ý cùng kiểu; tuple [string, number] cố định vị trí và kiểu.
  • Object type mô tả hình dạng: tên thuộc tính kèm kiểu của nó.

Một literal type là khi kiểu hẹp lại đúng bằng một giá trị cụ thể: không phải "một chuỗi bất kỳ" mà đúng chữ "do". Ghép nhiều literal bằng dấu | cho ta một tập giá trị hợp lệ:

literal.ts

let mau: "do" | "xanh" | "vang";
mau = "xanh";   // OK
mau = "tim";    // Loi: "tim" khong nam trong tap cho phep

Nhớ vụ widening ở Bước 1: let nới literal thành kiểu rộng (string), còn const giữ literal. Khi muốn khoá cả một object hay mảng lại thành literal, dùng as const:

as-const.ts

const mau1 = ["do", "xanh"];            // suy ra: string[]
const mau2 = ["do", "xanh"] as const;   // suy ra: readonly ["do", "xanh"]
  • Literal type: kiểu hẹp đúng bằng một giá trị; ghép bằng | thành tập giá trị hợp lệ.
  • let nới literal thành kiểu rộng; const giữ nguyên literal.
  • as const khoá object/mảng thành literal readonly - nền cho discriminated union sau này.

any là cửa thoát: nó tắt mọi kiểm tra kiểu. Gán gì cũng được, gọi gì cũng được, TS im lặng cho qua - đúng nghĩa vứt đi tấm lưới an toàn mà Bài 1 đã dựng. Lỗi lại trôi tới lúc chạy:

any biên dịch OK, nhưng chạy thì nổ

let a: any = "hello";
a.toFixed(2);   // tsc khong bao gi; "hello" lam gi co .toFixed

Kết quả khi chạy

TypeError: a.toFixed is not a function

unknown là phiên bản an toàn của any: cũng nhận mọi giá trị, nhưng TS bắt bạn kiểm tra trước khi dùng. Cùng đoạn trên với unknown, tsc chặn ngay:

unknown: tsc bắt kiểm trước khi dùng

let u: unknown = "hello";
u.toFixed(2);

tsc báo

error TS18046: 'u' is of type 'unknown'.

Muốn dùng u, phải thu hẹp kiểu (narrowing) cho TS biết chắc nó là gì - một kỹ thuật ta sẽ học sâu ở bài Union & narrowing:

unknown an toàn sau khi kiểm

let u: unknown = 3.14159;
if (typeof u === "number") {
  console.log(u.toFixed(2));   // OK - trong if, TS biet u la number
}

Kết quả khi chạy

3.14

Cuối cùng, never là kiểu của thứ không bao giờ có giá trị: một hàm luôn ném lỗi (không bao giờ trả về), hay một nhánh "lẽ ra không xảy ra".

never.ts

function boom(): never {
  throw new Error("dung lai");
}

Trung thực

Quy tắc gọn: tránh any (nó tắt TypeScript), ưu tiên unknown khi chưa biết kiểu, và để dành never cho hàm không trả về hoặc nhánh bất khả thi. Đôi khi any là lối thoát nhanh khi vội, nhưng mỗi any là một lỗ thủng trên lưới an toàn - dùng càng ít càng tốt.

Bạn vừa có bộ kiểu nền và hiểu rằng TypeScript tự đoán rất nhiều, chỉ cần chú thích ở chỗ nó chịu thua. Nhưng các object type ở Bước 2 mới chỉ là khởi đầu: khi hình dạng dữ liệu lớn dần, ta cần cách đặt tên và tái dùng chúng.

Bài tiếp theo: Interface & structural typing

Bài sau giới thiệu interface, type alias và structural typing - cách TypeScript so khớp kiểu theo HÌNH DẠNG chứ không theo tên, một điểm "under the hood" hay gây bất ngờ.

Câu hỏi thường gặp

Không. Hãy để TypeScript tự suy luận với những biến gán giá trị ngay - chú thích lúc đó chỉ là thừa và rườm rà. Bạn chỉ cần chú thích ở chỗ TS không đoán được hoặc đoán không đúng ý: tham số hàm, mảng/object khởi tạo rỗng, hoặc khi muốn ép một kiểu rộng/hẹp hơn.

Cả hai đều nhận mọi giá trị, nhưng any TẮT kiểm tra (bạn làm gì với nó TS cũng im lặng, nên lỗi trôi tới lúc chạy), còn unknown thì AN TOÀN: TS bắt bạn kiểm tra (narrowing) để biết chắc kiểu trước khi dùng. Khi cần một "ô chứa kiểu chưa biết", hãy dùng unknown thay cho any.

as const khoá một giá trị lại thành kiểu hẹp nhất: chuỗi thành literal đúng chữ đó, mảng thành tuple readonly. Rất hợp cho hằng số, cấu hình, và sau này là discriminated union - nơi bạn cần TS nhớ chính xác từng giá trị thay vì nới rộng thành string/number.

never là kiểu của thứ KHÔNG BAO GIỜ có giá trị: một hàm luôn ném lỗi hoặc lặp vô tận (không bao giờ trả về), hoặc một nhánh code "lẽ ra không thể xảy ra". Công dụng mạnh nhất của never là kiểm tra vét cạn (exhaustiveness), ta sẽ gặp ở bài về union và narrowing.

Array (number[]) là danh sách dài tuỳ ý, mọi phần tử cùng kiểu. Tuple ([string, number]) có ĐỘ DÀI CỐ ĐỊNH và mỗi vị trí một kiểu riêng - hợp khi thứ tự có ý nghĩa, vd trả về cặp [tên, tuổi].

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Vì sao nên tránh any và ưu tiên unknown?

Bài tập về nhà

  1. 1

    Để TS đoán hộ

    Viết 5 khai báo biến với giá trị ban đầu (số, chuỗi, mảng, object, boolean). KHÔNG chú thích kiểu. Dùng widget ở Bước 1 hoặc Playground để xem TS suy ra kiểu gì.

    ✅ Hoàn thành khi: Một bảng 5 dòng: khai báo của mèo con kèm kiểu TypeScript suy ra cho từng dòng.

  2. 2

    let và const khác kiểu

    Viết const ten = "Bo"; và let ten2 = "Bo";. Soi kiểu TS suy ra cho mỗi cái và giải thích vì sao khác nhau.

    ✅ Hoàn thành khi: Ghi đúng: const ra "Bo" (literal), let ra string, kèm một câu giải thích về widening.

  3. 3

    any nổ lúc chạy

    Gán một chuỗi vào biến kiểu any rồi gọi một method của số (vd .toFixed()). Biên dịch (không lỗi) rồi chạy bằng node để thấy nó nổ.

    ✅ Hoàn thành khi: Một file chạy ra TypeError lúc chạy, kèm một câu: vì sao any để lọt lỗi này còn unknown thì không.

  4. 4

    unknown buộc kiểm

    Đổi biến any ở bài trên thành unknown. Xem tsc báo lỗi gì, rồi thêm một kiểm tra typeof để code biên dịch lại được.

    ✅ Hoàn thành khi: Đoạn unknown ban đầu bị tsc chặn, và bản có thêm typeof thì biên dịch sạch.

  5. 5

    Khoá bằng as const

    Viết một mảng các chuỗi cố định (vd các màu), thêm as const. So kiểu suy ra trước và sau khi thêm as const.

    ✅ Hoàn thành khi: Ghi rõ: trước as const là string[], sau as const là readonly tuple các literal đúng chữ.

  6. 6

    Chọn chỗ chú thích

    Tìm trong code của mèo con 2 chỗ chú thích kiểu là THỪA (TS đã đoán được) và 1 chỗ chú thích là CẦN (TS không đoán được, vd tham số hàm).

    ✅ Hoàn thành khi: Hai ví dụ chú thích thừa nên bỏ, và một ví dụ chú thích cần giữ, kèm lý do mỗi chỗ.