Bài 10.5: Bài tập thực hành TypeScript cơ bản

Chào mừng trở lại với chuỗi bài viết về Lập trình Web Front-end! Sau khi đã làm quen với những khái niệm lý thuyết về TypeScript, giờ là lúc chúng ta bắt tay vào thực hành để củng cố kiến thức. Thực hành chính là chìa khóa để biến lý thuyết khô khan thành kỹ năng thực tế.

Trong bài này, chúng ta sẽ cùng nhau giải quyết một số bài tập nhỏ, tập trung vào các khái niệm TypeScript cơ bản nhất. Đừng lo lắng nếu bạn chưa nhớ hết mọi thứ, hãy coi đây là cơ hội để ôn tập và làm quen với cú pháp.

Hãy cùng bắt đầu!

Bài tập 1: Làm việc với Kiểu Dữ liệu Cơ bản

Mục tiêu của bài tập này là làm quen với việc khai báo biến và gán kiểu dữ liệu tường minh trong TypeScript.

Yêu cầu:

  1. Khai báo một biến userName kiểu string và gán giá trị là tên của bạn.
  2. Khai báo một biến userAge kiểu number và gán giá trị là tuổi của bạn.
  3. Khai báo một biến isStudent kiểu boolean và gán giá trị true hoặc false.
  4. Thử gán một giá trị sai kiểu cho một trong các biến trên và xem TypeScript báo lỗi như thế nào.

Code mẫu và Giải thích:

// 1. Khai báo biến userName kiểu string
let userName: string = "Alice";
console.log(`Tên người dùng: ${userName}`); // Output: Tên người dùng: Alice

// 2. Khai báo biến userAge kiểu number
let userAge: number = 28;
console.log(`Tuổi người dùng: ${userAge}`); // Output: Tuổi người dùng: 28

// 3. Khai báo biến isStudent kiểu boolean
let isStudent: boolean = false;
console.log(`Là sinh viên: ${isStudent}`); // Output: Là sinh viên: false

// 4. Thử gán sai kiểu (TypeScript sẽ báo lỗi biên dịch)
// userAge = "ba muoi"; // <-- Nếu bỏ comment dòng này, TypeScript sẽ báo lỗi!
// console.log(`Tuổi người dùng (sai kiểu): ${userAge}`);

Giải thích:

Trong đoạn code trên, chúng ta sử dụng cú pháp : type ngay sau tên biến khi khai báo để chỉ định rõ kiểu dữ liệu.

  • let userName: string = "Alice";: Biến userName bắt buộc phải là kiểu chuỗi.
  • let userAge: number = 28;: Biến userAge bắt buộc phải là kiểu số.
  • let isStudent: boolean = false;: Biến isStudent bắt buộc phải là kiểu boolean.

Khi bạn thử gán "ba muoi" (kiểu string) vào biến userAge (kiểu number), trình biên dịch TypeScript sẽ ngay lập tức báo lỗi, giúp bạn phát hiện sai sót trước khi chạy chương trình. Đây chính là sức mạnh của việc định kiểu tĩnh!

Bài tập 2: Mảng và Tuple

TypeScript cung cấp cách định kiểu mạnh mẽ cho cả mảng và Tuple.

Yêu cầu:

  1. Khai báo một mảng tên là colors chứa chỉ các giá trị kiểu string. Gán một vài màu vào đó.
  2. Khai báo một mảng tên là primeNumbers chứa chỉ các giá trị kiểu number. Gán một vài số nguyên tố vào đó.
  3. Khai báo một Tuple tên là coordinates với phần tử đầu tiên là number (tọa độ x) và phần tử thứ hai là number (tọa độ y). Gán giá trị cho nó.
  4. Thử thêm một phần tử sai kiểu vào một trong các mảng hoặc gán sai kiểu cho Tuple.

Code mẫu và Giải thích:

// 1. Mảng chỉ chứa string
let colors: string[] = ["Red", "Green", "Blue"];
console.log("Mảng màu:", colors); // Output: Mảng màu: [ 'Red', 'Green', 'Blue' ]

// 2. Mảng chỉ chứa number
let primeNumbers: number[] = [2, 3, 5, 7, 11];
console.log("Mảng số nguyên tố:", primeNumbers); // Output: Mảng số nguyên tố: [ 2, 3, 5, 7, 11 ]

// Thử thêm sai kiểu vào mảng (báo lỗi)
// colors.push(123); // <-- Lỗi! Argument of type 'number' is not assignable to parameter of type 'string'.

// 3. Tuple với 2 phần tử number
let coordinates: [number, number] = [10.5, 20.3];
console.log("Tọa độ:", coordinates); // Output: Tọa độ: [ 10.5, 20.3 ]

// Thử gán sai kiểu hoặc sai cấu trúc cho Tuple (báo lỗi)
// coordinates = [5, "hello"]; // <-- Lỗi! Type '[number, string]' is not assignable to type '[number, number]'.
// coordinates[0] = "zero"; // <-- Lỗi! Type 'string' is not assignable to type 'number'.

Giải thích:

  • string[] hoặc Array<string>: Đây là cách khai báo một mảng mà tất cả các phần tử bên trong đều phải là kiểu string. Tương tự với number[]. TypeScript sẽ ngăn chặn việc thêm một phần tử có kiểu khác vào mảng này.
  • [number, number]: Đây là cách khai báo một Tuple. Nó là một mảng có số lượng phần tử cố định (ở đây là 2) và kiểu dữ liệu của từng phần tử ở từng vị trí cố định (phần tử 0 là number, phần tử 1 là number).

Việc sử dụng kiểu mảng và Tuple giúp code của bạn trở nên có cấu trúc hơn và dễ dự đoán hơn.

Bài tập 3: Kiểu anyunknown

Mặc dù mục tiêu của TypeScript là định kiểu rõ ràng, đôi khi bạn cần xử lý các giá trị mà bạn không biết trước kiểu của chúng. Kiểu anyunknown ra đời để giải quyết vấn đề này, nhưng với sự khác biệt quan trọng.

Yêu cầu:

  1. Khai báo một biến data kiểu any và gán cho nó một số, sau đó gán lại cho nó một chuỗi.
  2. Khai báo một biến input kiểu unknown và gán cho nó một số.
  3. Thử truy cập một thuộc tính (ví dụ: .length) hoặc gọi một phương thức (ví dụ: .toUpperCase()) trên biến data (kiểu any) và biến input (kiểu unknown) mà không kiểm tra kiểu. Quan sát sự khác biệt.
  4. Thử kiểm tra kiểu của biến input và sau đó thực hiện một hành động dựa trên kiểu đã kiểm tra.

Code mẫu và Giải thích:

// 1. Biến kiểu any - linh hoạt nhưng mất an toàn kiểu
let data: any = 100; // Gán số
console.log("Data (number):", data); // Output: Data (number): 100
data = "hello world"; // Gán chuỗi - OK
console.log("Data (string):", data); // Output: Data (string): hello world

// Thử gọi phương thức string trên biến any (TypeScript không báo lỗi ở đây!)
// data.toUpperCase(); // <-- TypeScript KHÔNG báo lỗi, nhưng sẽ lỗi runtime nếu data không phải string!
console.log("Data (uppercase):", data.toUpperCase()); // Output: Data (uppercase): HELLO WORLD (OK vì hiện tại là string)

// 2. Biến kiểu unknown - an toàn hơn
let input: unknown = 500; // Gán số
console.log("Input (number):", input); // Output: Input (number): 500
input = "another value"; // Gán chuỗi - OK
console.log("Input (string):", input); // Output: Input (string): another value

// Thử gọi phương thức string trên biến unknown (TypeScript BÁO LỖI!)
// input.toUpperCase(); // <-- Lỗi! 'input' is of type 'unknown'.

// 4. Kiểm tra kiểu trước khi sử dụng biến unknown
if (typeof input === 'string') {
    // Bên trong block if này, TypeScript biết 'input' là string
    console.log("Input (uppercase sau kiểm tra):", input.toUpperCase()); // Output: Input (uppercase sau kiểm tra): ANOTHER VALUE
} else if (typeof input === 'number') {
    console.log("Input (cộng 10 sau kiểm tra):", input + 10); // Output: Input (cộng 10 sau kiểm tra): 510 (nếu input là number)
}

Giải thích:

  • any: Khi sử dụng any, bạn đang nói với TypeScript rằng "tôi biết tôi đang làm gì, hãy bỏ qua việc kiểm tra kiểu cho biến này". Biến any có thể nhận bất kỳ giá trị nào và bạn có thể truy cập bất kỳ thuộc tính hoặc phương thức nào trên nó mà không bị lỗi biên dịch. Tuy nhiên, điều này rất nguy hiểm vì lỗi sai kiểu sẽ chỉ xuất hiện khi chạy chương trình (runtime), khiến việc debug khó khăn hơn.
  • unknown: Ngược lại, unknown cũng cho phép biến nhận bất kỳ giá trị nào, nhưng nó buộc bạn phải kiểm tra kiểu trước khi có thể làm bất cứ điều gì có ý nghĩa với biến đó (truy cập thuộc tính, gọi phương thức). Điều này làm cho unknown an toàn hơn nhiều so với any khi bạn không chắc chắn về kiểu dữ liệu đầu vào. Bạn phải sử dụng các kỹ thuật như typeof hoặc type assertion để làm việc với giá trị unknown.

Nguyên tắc chung: Hãy cố gắng tránh sử dụng any nhiều nhất có thể. Nếu bạn không biết chính xác kiểu, hãy sử dụng unknown và thực hiện kiểm tra kiểu.

Bài tập 4: Hàm với Kiểu Dữ liệu Rõ ràng

Định nghĩa kiểu cho tham số và giá trị trả về của hàm là một trong những lợi ích lớn nhất của TypeScript.

Yêu cầu:

  1. Viết một hàm addNumbers nhận hai tham số, cả hai đều là number, và trả về tổng của chúng (cũng là number).
  2. Viết một hàm greet nhận một tham số name kiểu string và không trả về giá trị nào (sử dụng kiểu void). Hàm này chỉ in ra lời chào.
  3. Viết một hàm getUserInfo nhận userId kiểu number và trả về một object có kiểu string hoặc undefined (giả định tìm thấy hoặc không tìm thấy user). Sử dụng union type.

Code mẫu và Giải thích:

// 1. Hàm cộng hai số
function addNumbers(a: number, b: number): number {
    return a + b;
}

let sum: number = addNumbers(5, 10);
console.log("Tổng:", sum); // Output: Tổng: 15

// Thử gọi hàm với sai kiểu (báo lỗi)
// addNumbers("hello", 5); // <-- Lỗi! Argument of type 'string' is not assignable to parameter of type 'number'.

// 2. Hàm chào (không trả về giá trị)
function greet(name: string): void {
    console.log(`Xin chào, ${name}!`);
}

greet("Bob"); // Output: Xin chào, Bob!

// 3. Hàm trả về string hoặc undefined
function getUserInfo(userId: number): string | undefined {
    if (userId === 1) {
        return "User ID 1: Alice";
    } else {
        return undefined; // Không tìm thấy user
    }
}

let userInfo1 = getUserInfo(1);
let userInfo2 = getUserInfo(99);

console.log("User Info 1:", userInfo1); // Output: User Info 1: User ID 1: Alice
console.log("User Info 2:", userInfo2); // Output: User Info 2: undefined

// Kiểm tra giá trị trả về trước khi sử dụng
if (userInfo1 !== undefined) {
    console.log("userInfo1 tồn tại:", userInfo1.toUpperCase()); // Output: userInfo1 tồn tại: USER ID 1: ALICE
}

Giải thích:

  • function functionName(paramName: paramType): returnType { ... }: Đây là cú pháp đầy đủ để định nghĩa kiểu cho hàm.
    • : paramType: Chỉ định kiểu dữ liệu cho tham số.
    • : returnType: Chỉ định kiểu dữ liệu mà hàm sẽ trả về. Nếu hàm không trả về gì, sử dụng void.
  • string | undefined: Đây là union type. Nó cho phép biến hoặc giá trị có thể là một trong số các kiểu được liệt kê (hoặc string hoặc undefined). TypeScript yêu cầu bạn xử lý tất cả các khả năng khi làm việc với biến/giá trị kiểu union (ví dụ: kiểm tra !== undefined).

Việc định kiểu cho hàm giúp tài liệu hóa mục đích của hàm rõ ràng hơn và giúp TypeScript kiểm tra tính đúng đắn của việc gọi hàm.

Bài tập 5: Type Aliases và Interfaces

Type AliasesInterfaces cho phép bạn định nghĩa các kiểu dữ liệu tùy chỉnh phức tạp hơn, đặc biệt hữu ích cho các object.

Yêu cầu:

  1. Sử dụng type để tạo một alias tên là Point cho kiểu object có hai thuộc tính xy, cả hai đều là number.
  2. Tạo một biến origin sử dụng alias Point.
  3. Sử dụng interface để định nghĩa một giao diện tên là Car với các thuộc tính make (string), model (string) và year (number, tùy chọn).
  4. Tạo một object myCar tuân thủ giao diện Car.

Code mẫu và Giải thích:

// 1. Sử dụng Type Alias cho object
type Point = {
    x: number;
    y: number;
};

// 2. Tạo biến sử dụng alias Point
let origin: Point = { x: 0, y: 0 };
console.log("Điểm gốc:", origin); // Output: Điểm gốc: { x: 0, y: 0 }

// Thử tạo Point sai cấu trúc (báo lỗi)
// let invalidPoint: Point = { x: 10 }; // <-- Lỗi! Property 'y' is missing.

// 3. Sử dụng Interface cho object
interface Car {
    make: string;
    model: string;
    year?: number; // Dấu '?' biến thuộc tính thành tùy chọn (optional)
}

// 4. Tạo object tuân thủ interface Car
let myCar: Car = {
    make: "Toyota",
    model: "Camry"
    // year là tùy chọn, không cần khai báo ở đây vẫn được
};

let anotherCar: Car = {
    make: "Honda",
    model: "Civic",
    year: 2020
};

console.log("Xe của tôi:", myCar); // Output: Xe của tôi: { make: 'Toyota', model: 'Camry' }
console.log("Xe khác:", anotherCar); // Output: Xe khác: { make: 'Honda', model: 'Civic', year: 2020 }

// Thử tạo object không tuân thủ interface (báo lỗi)
// let invalidCar: Car = { make: "Ford" }; // <-- Lỗi! Property 'model' is missing.

Giải thích:

  • type Point = { ... }: type là một cách tạo ra một tên mới cho một kiểu dữ liệu có sẵn hoặc một kiểu dữ liệu tự định nghĩa (ở đây là kiểu object có xynumber). Bạn có thể sử dụng alias này ở bất cứ đâu thay vì phải viết lại định nghĩa kiểu object.
  • interface Car { ... }: interface cũng là một cách định nghĩa cấu trúc của object. interface thường được sử dụng để định nghĩa "hợp đồng" mà một object hoặc class phải tuân theo. Sự khác biệt giữa typeinterface khá tinh tế và đôi khi có thể sử dụng thay thế cho nhau, nhưng interface có khả năng mở rộng (extends) và implement bởi class, điều mà type không làm được theo cùng cách.
  • year?: number: Dấu ? sau tên thuộc tính (year) chỉ ra rằng thuộc tính này là tùy chọn (optional). Một object tuân thủ interface Car có thể có hoặc không có thuộc tính year.

Sử dụng typeinterface giúp bạn quản lý các kiểu object phức tạp một cách ngăn nắp và dễ đọc.

Bài tập 6: Enum

Enum (Enumeration) cho phép bạn định nghĩa một tập hợp các hằng số được đặt tên. Điều này làm cho code dễ đọc và dễ bảo trì hơn khi làm việc với các tập giá trị cố định.

Yêu cầu:

  1. Định nghĩa một enum tên là Status với các giá trị Pending, Processing, Completed, Failed.
  2. Khai báo một biến orderStatus sử dụng Status và gán giá trị Processing.
  3. In giá trị của orderStatus ra console.

Code mẫu và Giải thích:

// 1. Định nghĩa Enum Status
enum Status {
    Pending,     // Mặc định là 0
    Processing,  // Mặc định là 1
    Completed,   // Mặc định là 2
    Failed       // Mặc định là 3
}

// Hoặc gán giá trị cụ thể (ví dụ: chuỗi)
enum HttpMethod {
    GET = "GET",
    POST = "POST",
    PUT = "PUT",
    DELETE = "DELETE"
}


// 2. Khai báo biến sử dụng Enum Status
let orderStatus: Status = Status.Processing;

// 3. In giá trị Enum
console.log("Trạng thái đơn hàng (giá trị số):", orderStatus); // Output: Trạng thái đơn hàng (giá trị số): 1
console.log("Trạng thái đơn hàng (tên):", Status[orderStatus]); // Output: Trạng thái đơn hàng (tên): Processing

// Sử dụng HttpMethod enum
let requestMethod: HttpMethod = HttpMethod.POST;
console.log("Phương thức HTTP:", requestMethod); // Output: Phương thức HTTP: POST

Giải thích:

  • enum Status { ... }: Từ khóa enum được dùng để tạo một tập hợp các hằng số. Mặc định, các thành viên của enum được gán giá trị số tăng dần bắt đầu từ 0.
  • Bạn có thể truy cập các giá trị của enum bằng cách sử dụng cú pháp EnumName.MemberName (ví dụ: Status.Processing). Giá trị trả về sẽ là giá trị số (1 trong trường hợp này).
  • Bạn cũng có thể lấy tên của thành viên từ giá trị số bằng cú pháp EnumName[value] (ví dụ: Status[1] sẽ trả về "Processing").
  • Bạn có thể gán giá trị cụ thể cho các thành viên của enum, ví dụ như chuỗi, như trong HttpMethod.

Enum giúp code của bạn dễ đọc hơn nhiều so với việc sử dụng các "số ma thuật" hoặc chuỗi lặp đi lặp lại để biểu diễn các trạng thái hoặc lựa chọn cố định.

Comments

There are no comments at the moment.