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

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:
- Khai báo một biến
userName
kiểustring
và gán giá trị là tên của bạn. - Khai báo một biến
userAge
kiểunumber
và gán giá trị là tuổi của bạn. - Khai báo một biến
isStudent
kiểuboolean
và gán giá trịtrue
hoặcfalse
. - 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ếnuserName
bắt buộc phải là kiểu chuỗi.let userAge: number = 28;
: BiếnuserAge
bắt buộc phải là kiểu số.let isStudent: boolean = false;
: BiếnisStudent
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:
- Khai báo một mảng tên là
colors
chứa chỉ các giá trị kiểustring
. Gán một vài màu vào đó. - Khai báo một mảng tên là
primeNumbers
chứa chỉ các giá trị kiểunumber
. Gán một vài số nguyên tố vào đó. - 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ó. - 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ặcArray<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ểustring
. Tương tự vớinumber[]
. 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 any
và unknown
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 any
và unknown
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:
- Khai báo một biến
data
kiểuany
và gán cho nó một số, sau đó gán lại cho nó một chuỗi. - Khai báo một biến
input
kiểuunknown
và gán cho nó một số. - 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ếndata
(kiểuany
) và biếninput
(kiểuunknown
) mà không kiểm tra kiểu. Quan sát sự khác biệt. - 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ụngany
, 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ếnany
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 chounknown
an toàn hơn nhiều so vớiany
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:
- 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
). - Viết một hàm
greet
nhận một tham sốname
kiểustring
và không trả về giá trị nào (sử dụng kiểuvoid
). Hàm này chỉ in ra lời chào. - Viết một hàm
getUserInfo
nhậnuserId
kiểunumber
và trả về một object có kiểustring
hoặcundefined
(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ụngvoid
.
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ặcstring
hoặcundefined
). 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 Aliases
và Interfaces
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:
- Sử dụng
type
để tạo một alias tên làPoint
cho kiểu object có hai thuộc tínhx
vày
, cả hai đều lànumber
. - Tạo một biến
origin
sử dụng aliasPoint
. - Sử dụng
interface
để định nghĩa một giao diện tên làCar
với các thuộc tínhmake
(string),model
(string) vàyear
(number, tùy chọn). - Tạo một object
myCar
tuân thủ giao diệnCar
.
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óx
vày
lànumber
). 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ữatype
vàinterface
khá tinh tế và đôi khi có thể sử dụng thay thế cho nhau, nhưnginterface
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ủ interfaceCar
có thể có hoặc không có thuộc tínhyear
.
Sử dụng type
và interface
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:
- Định nghĩa một
enum
tên làStatus
với các giá trịPending
,Processing
,Completed
,Failed
. - Khai báo một biến
orderStatus
sử dụngStatus
và gán giá trịProcessing
. - 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óaenum
đượ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