Nhìn lại lịch sử, JavaScript là ngôn ngữ chính để viết web vì nó có thể sử dụng được trên cả front-end lẫn back-end của các framework như Node.js và Deno. Nhưng khi các trang web phát triển thành một hệ thống lớn và phức tạp hơn, thì sử dụng JavaScript sẽ gặp khó khăn trong việc đọc code và bảo trì.
Microsoft đã sớm nhận thấy vấn đề này và giải pháp của họ là TypeScript! Từ phiên bản đầu tiên được phát hành năm 2012, TypeScript ngày càng trở nên phổ biến và càng có nhiều công việc đòi hỏi các developer phải biết TypeScript.
Theo báo cáo Developer Survey 2021 của Stackoverflow, TypeScript là ngôn ngữ phổ biến thứ 5 đối với các developer chuyên nghiệp. TypeScript cũng đứng thứ 3 trong danh sách “những ngôn ngữ được yêu thích” và thứ 2 trong “những ngôn ngữ mong muốn sử dụng” đối với developer.
Do đó, học hỏi và trau dồi kiến thức về TypeScript sẽ không phí thời gian của bạn, đặc biệt nếu bạn là web developer đang tìm kiếm những cơ hội việc làm hoặc cơ hội thăng tiến tốt hơn.
1. TypeScript là gì và nó khác với JavaScript như thế nào?
TypeScript là một superset (tập mẫu) của JavaScript, có thể biên dịch sang JavaScript thuần. Về mặt khái niệm, mối quan hệ giữa TypeScript và JavaScript có thể so sánh với mối quan hệ của SASS và CSS.
Nói cách khác, TypeScript là phiên bản ES6 của JavaScript với một số tính năng bổ sung.
TypeScript là một ngôn ngữ nhập tĩnh (Statical typed) và hướng đối tượng (Object oriented), tương tự như Java và C#. Trong khi đó, JavaScript là một ngôn ngữ kịch bản (Scripting language) gần giống Python. Bản chất hướng đối tượng của TypeScript trở nên hoàn thiện với các tính năng như class và interface. Tính năng nhập tĩnh của TypeScript cho phép hiệu chỉnh tốt hơn thông qua việc suy luận kiểu (Type inference) tuỳ theo ý của bạn.
Về mặt code, TypeScript được viết trong tệp có đuôi .ts, trong khi JavaScript có đuôi .js. Không giống như JavaScript, trình duyệt sẽ không thể hiểu được code trong TypeScript, loại code này cũng không thể thực thi trực tiếp trong trình duyệt hoặc bất kỳ nền tảng nào khác.
Do đó, trước tiên, các tệp .ts cần phải được biên dịch sang JavaScript thuần, thông qua trình biên dịch tsc của TypeScript. Sau đó, chúng sẽ được thực thi bởi nền tảng đích.
2. Lợi ích của việc sử dụng TypeScript là gì?
Một lợi thế dễ thấy của TypeScript là tooling. TypeScript là một ngôn ngữ strong typing (Nghĩa là dạng của đối tượng được giữ nguyên trừ khi có lệnh rõ ràng yêu cầu thay đổi) và sử dụng type inference (Suy luận kiểu). Những đặc điểm này giúp tooling tốt hơn và tích hợp chặt chẽ hơn với các trình sửa code. Sự kiểm tra nghiêm ngặt của TypeScript giúp phát hiện sớm các lỗi, giảm đáng kể khả năng mắc lỗi chính tả và các lỗi khác do con người gây ra.
Từ góc độ của IDE (Integrated development environment – Môi trường phát triển tích hợp), TypeScript giúp IDE hiểu code tốt hơn bằng cách cho phép nó hiển thị gợi ý, cảnh báo và lỗi sai rõ ràng hơn đến developer.
Ví dụ: TypeScript kiểm tra null và báo lỗi ngay tại thời điểm biên dịch (Trong IDE của bạn), nhờ đó ngăn chặn một lỗi phổ biến trong JavaScript đó là truy cập vào thuộc tính của một biến không xác định trong thời gian chạy.
Lợi ích lâu dài của việc sử dụng TypeScript là khả năng mở rộng và khả năng bảo trì. Khả năng mô tả cụ thể các đối tượng và hàm trực tiếp trong code giúp cho codebase của bạn trở nên dễ hiểu hơn, dễ dự đoán hơn. Khi được sử dụng đúng cách, TypeScript cung cấp một ngôn ngữ chuẩn hóa giúp developer đọc code tốt hơn, từ đó có thể tiết kiệm thời gian và công sức khi codebase tiếp tục phát triển.
3. Interface trong TypeScript là gì?
Interface là cách TypeScript xác định cú pháp của các thực thể. Nói cách khác, interface là một cách để mô tả các data shape như objects (Đối tượng) hoặc array of objects (Mảng đối tượng).
Bạn có thể thiết lập interface bằng từ khóa interface, tiếp đến là tên và định nghĩa. Cùng xem cách thiết lập một interface đơn giản cho object
User:interface User { name: string; age: number; }
Sau đó, interface này có thể dùng để đặt kiểu cho một biến (Tương tự như cách bạn gán kiểu nguyên thủy – primitive type cho biến). Khi đó, một biến có type
User sẽ tuân theo các thuộc tính của interface.let user: User = { name: “Bob”, age: 20, // omitting the `age` property or assigning a different type // instead of a number would throw an error };
Các interface giúp thúc đẩy tính nhất quán trong dự án TypeScript. Thêm nữa, các interface cũng giúp cải thiện tooling của dự án, cung cấp chức năng autocomplete trong IDE tốt hơn và đảm bảo các giá trị đúng đang được truyền vào constructor và hàm.
4. Làm thế nào để tạo type mới bằng một tập hợp con của interface?
Việc sửa đổi interface rất có lợi để loại bỏ code trùng lặp và tối đa hoá khả năng tái sử dụng của các interface hiện có (Nếu điều đó hợp lý). Câu hỏi này nhắc đến một trong nhiều tính năng mà TypeScript cung cấp để tạo ra một interface mới từ interface hiện có.
TypeScript có một utility type gọi là
omit (Bỏ qua), cho phép bạn tạo ra một type mới bằng cách pass một type/ interface đang có và chọn lựa các key sẽ được loại bỏ khỏi type mới.Ví dụ dưới đây cho thấy cách tạo ra type mới “UserPreview” dựa trên interface “User” đã có, nhưng đã được loại bỏ thuộc tính email:
interface User { name: string; description: string; age: number; email: string; } // removes the `email` property from the User interface type UserPreview = Omit<User, “email”>; const userPreview: UserPreview = { name: “Bob”, description: “Awesome guy”, age: 20, };
4. Các “enum” hoạt động như thế nào trong TypeScript?
Enum là một cấu trúc dữ liệu phổ biến trong hầu hết các ngôn ngữ typed. Hiểu rõ về enum và cách sử dụng chúng là một phần quan trọng trong việc tổ chức code và giúp code trở nên dễ đọc hơn.
Enum – hay còn gọi là enumerated types (Kiểu liệt kê) là một phương tiện để xác định một tập hợp các hằng số được đặt tên. Các cấu trúc dữ liệu này có độ dài không đổi và chứa một tập hợp các giá trị không đổi. Enum trong TypeScript thường được dùng để biểu diễn một số lượng nhất định các tùy chọn cho một giá trị cho trước, thông qua một tập hợp các cặp key/ value.
Hãy xem ví dụ về enum dùng để xác định một tập hợp các
user typeenum UserType { Guest = “G”, Verified = “V”, Admin = “A”, } const userType: UserType = UserType.Verified;
Ở bên dưới, TypeScript sẽ biên dịch các enum thành các đối tượng JavaScript thuần. Điều này khiến việc sử dụng enum có lợi hơn so với sử dụng nhiều biến const độc lập. Nhóm mà enum tạo ra giúp cho code của bạn an toàn về type và dễ đọc hơn.
5. Hàm arrow (Hàm mũi tên) trong TypeScript là gì?
Arrow function là một tính năng phổ biến của ES6 và TypeScript nhằm giới thiệu cách khác ngắn hơn để xác định các hàm. Arrow function cũng có những ưu và nhược điểm quan trọng mà bạn cần xem xét khi lựa chọn phương pháp nào sẽ được sử dụng.
Hàm arrow – hay còn được gọi là hàm lambda, cung cấp một cú pháp ngắn gọn và thuận tiện để khai báo các hàm. Các hàm arrow thường được sử dụng để tạo các hàm gọi lại (Callback function) trong TypeScript. Các phép toán mảng (array operations) như map, filter, và reduce đều chấp nhận các hàm arrow làm đối số của chúng.
Tuy nhiên, tính ẩn danh của hàm arrow cũng có mặt trái. Nếu dùng không đúng, cú pháp ngắn hơn có thể gây khó hiểu hơn. Hơn nữa, bản chất không tên của các hàm arrow cũng khiến nó không thể tạo các hàm tự tham chiếu (Tức là đệ quy).
Chúng ta hãy xem cách một hàm chính quy (Regular function) chấp nhận hai số và trả về tổng của nó.
function addNumbers(x: number, y: number): number { return x + y; } addNumbers(1, 2); // returns 3
Bây giờ, hãy chuyển đổi hàm trên thành một hàm arrow:
const addNumbers = (x: number, y: number): number => { return x + y; }; addNumbers(1, 2); // returns 3
6. Sự khác nhau giữa var, let và const trong TypeScript là gì?
TypeScript cung cấp ba cách khai báo biến khác nhau: var, let và const. Phân biệt được ba từ khóa này, hiểu được khi nào nên dùng từ nào là yêu cầu quan trọng để viết code chất lượng. Hãy đảm bảo rằng bạn có trình bày về các trường hợp sử dụng cụ thể đối với từng từ khóa, kèm theo tính chất của chúng.
- var: Khai báo biến function scope hoặc biến toàn cục (Global scope), có tính chất và quy tắc phạm vi tương tự với các biến var của JavaScript. Các biến var không yêu cầu gán giá trị cho nó trong quá trình khai báo.
- let: Khai báo một biến cục bộ trong phạm vi khối (Block-scoped local variable). Các biến let không yêu cầu gán giá trị cho một biến trong quá trình khai báo. Block-scoped local variable có nghĩa là biến chỉ có thể được truy cập trong khối chứa của nó, chẳng hạn như một hàm, một khối if/ else hoặc một vòng lặp. Hơn nữa, không giống như var, chúng ta không thể đọc hoặc ghi biến let trước khi chúng được khai báo.
// reading/writing before a `let` variable is declared console.log(“age”, age); // Compiler Error: error TS2448: Block-scoped variable ‘age’ used before its declaration age = 20; // Compiler Error: error TS2448: Block-scoped variable ‘age’ used before its declaration let age: number; // accessing `let` variable outside its scope function user(): void { let name: string; if (true) { let email: string; console.log(name); // OK console.log(email); // OK } console.log(name); // OK console.log(email); // Compiler Error: Cannot find name ’email’ }
- const: Khai báo một giá trị hằng số trong phạm vi khối mà giá trị đó không thể thay đổi sau khi khởi tạo. Các biến const yêu cầu việc khởi tạo như một phần trong quá trình khai báo. Điều này trở nên lý tưởng đối với các biến không thay đổi trong suốt thời gian tồn tại của chúng.
// reassigning a `const` variable const age: number = 20; age = 30; // Compiler Error: Cannot assign to ‘age’ because it is a constant or read-only property // declaring a `const` variable without initialization const name: string; // Compiler Error: const declaration must be initialized
7. Khi nào bạn sử dụng return type là “never” và nó khác với “void” như thế nào?
Trước khi đi sâu vào sự khác biệt giữa never và void, cùng nói về hoạt động của một hàm JavaScript khi không có gì được trả về rõ ràng.
Hãy xem qua hàm trong ví dụ bên dưới. Nó không trả lại bất cứ điều gì rõ ràng cho caller function (Hàm gọi). Tuy nhiên, nếu bạn gán nó cho một biến và ghi lại giá trị của biến đó, bạn sẽ thấy rằng giá trị của hàm là undefined (Không xác định).
printName(name: string): void { console.log(name); } const printer = printName('Mike'); // Mike console.log(printer); // logs “undefined”
Đoạn mã trên là một ví dụ về void. Các hàm không trả về rõ ràng được TypeScript suy luận có return type là void.
Ngược lại, never là hàm đại diện cho một giá trị không bao giờ xảy ra. Ví dụ, một hàm có vòng lặp vô hạn hoặc một hàm báo lỗi là những hàm có return type là never.
const error = (): never => { throw new Error(''); };
Tóm lại, void được sử dụng khi một hàm không trả về bất kỳ thứ gì rõ ràng, còn never được sử dụng khi một hàm không bao giờ trả về.
8. TypeScript hỗ trợ những Access modifier (Phạm vi truy cập) nào?
Access modifier (Phạm vi truy cập) và encapsulation (Sự đóng gói) song hành với nhau. Câu hỏi này nhằm để hiểu kiến thức của ứng viên về khái niệm “đóng gói”, cũng như về cách tiếp cận của TypeScript với việc hạn chế truy cập dữ liệu. Người hỏi cũng có thể đang muốn nghe về các ứng dụng thực tế của phạm vi truy cập, lý do đằng sau và lợi ích của chúng.
Khái niệm “encapsulation” (Đóng gói) được sử dụng trong lập trình hướng đối tượng, nhằm kiểm soát khả năng hiển thị những thuộc tính và phương thức của các đối tượng. TypeScript sử dụng các access modifier để cài đặt khả năng hiển thị nội dung của một class. Vì TypeScript được biên dịch sang JavaScript, logic liên quan đến phạm vi truy cập được áp dụng trong thời gian biên dịch, không phải trong lúc chạy.
Có ba loại access modifier trong TypeScript là: public, private, và protected.
- public: Tất cả các thuộc tính và phương thức được mặc định công khai. Chúng ta có thể nhìn thấy và truy cập các member công khai của một class từ bất kỳ vị trí nào.
- protected: Các thuộc tính được bảo vệ có thể truy cập được từ trong cùng một class và subclass của nó.
- private: Các thuộc tính riêng tư chỉ có thể truy cập được từ trong class mà thuộc tính hoặc phương thức đó được định nghĩa.
Để sử dụng bất kỳ phạm vi truy cập nào trong số trên, hãy thêm từ khoá public, private, và protected đằng trước thuộc tính hoặc phương thức (nếu bị bỏ qua, TypeScript sẽ mặc định là public)
class User { // only accessible inside the `User` class private username; // only accessible inside the `User` class and its subclass protected updateUser(): void {} // accessible from any location public getUser() {} }
Vi phạm các quy tắc của phạm vi truy cập, ví dụ như cố gắng truy cập thuộc tính private của một class từ một class khác sẽ dẫn đến lỗi dưới đây trong quá trình biên dịch
Property ‘<property-name>’ is private and only accessible within class ‘<class-name>’.
Tóm lại, phạm vi truy cập đóng một vai trò quan trọng trong việc sắp xếp code. Chúng cho phép hiển thị một tập hợp các API công khai và ẩn đi các chi tiết triển khai. Bạn có thể xem phạm vi truy cập như những cánh cổng dẫn vào class. Sử dụng hiệu quả các phạm vi truy cập sẽ làm giảm đáng kể khả năng xảy ra lỗi do sử dụng nhầm nội dung của class khác.
9. Generic là gì và cách sử dụng chúng trong TypeScript?
Những bài học “đắt giá” về kỹ thuật phần mềm thường khuyến khích khả năng tái sử dụng và tính linh hoạt. Việc sử dụng generic mang đến khả năng tái sử dụng và tính linh hoạt bằng cách: Cho phép một component hoạt động trên nhiều type thay vì một type duy nhất, mà vẫn giữ được độ chính xác (Không giống như việc sử dụng hàm any).
Dưới đây là một ví dụ về một hàm generic cho phép caller xác định type sẽ được sử dụng trong hàm.
function updateUser<Type>(arg: Type): Type { return arg; }
Để gọi một hàm generic, bạn có thể truyền trực tiếp trong type thông qua dấu ngoặc nhọn <> hoặc thông qua type argument inference (Cho phép TypeScript suy ra type dựa trên kiểu của đối số được truyền vào).
// explicitly specifying the type let user = updateUser<string>(“Bob”); // type argument inference let user = updateUser(“Bob”);
Generic cho phép chúng ta theo dõi thông tin type trong hàm. Điều này làm cho code trở nên linh hoạt và có thể tái sử dụng mà không ảnh hưởng đến độ chính xác của loại code.
