Để sự chuẩn bị tốt cho buổi phỏng vấn, bạn cần nắm vững kiến thức về TypeScript từ cơ bản đến nâng cao. Bộ câu hỏi phỏng vấn TypeScript sau đây bao gồm các câu hỏi về cú pháp, thực hành mã, giải đáp về type safety hay cách xử lý tình huống khi dùng TypeScript trong lập trình sẽ giúp bạn có góc nhìn toàn diện hơn về TypeScript.

Đọc bài viết sau đây để được giải đáp chi tiết hơn về:

  • Câu hỏi phỏng vấn TypeScript dành cho Fresher
  • Câu hỏi phỏng vấn TypeScript phù hợp với Junior hoặc Senior Developer
  • Câu hỏi phỏng vấn TypeScript về type safety
  • Câu hỏi phỏng vấn TypeScript về type inference
  • Câu hỏi phỏng vấn TypeScript xử lý tình huống

Tổng quan về TypeScript

TypeScript là một ngôn ngữ nguồn mở được phát triển bởi Anders Hejlsberg tại Microsoft. Đây là một siêu tập JavaScript được gõ tĩnh, biên dịch thành JavaScript thuần túy. Mã TypeScript sau khi biên dịch có thể chạy trên mọi trình duyệt, máy chủ và hệ điều hành hỗ trợ JavaScript.

Điều đó cũng có nghĩa là tất cả mã JavaScript đều hợp lệ cho TypeScript, giúp cung cấp các tính năng nâng cao như IntelliSense, hoàn thành mã hoặc tái cấu trúc mã an toàn,…

TypeScript bổ sung các tính năng ngôn ngữ và kiểu tĩnh tùy chọn như lớp và mô-đun. Điều quan trọng cần biết là tất cả các tính năng nâng cao này không tốn chi phí cho JavaScript. Sau khi biên dịch, kết quả sẽ là mã JavaScript thuần túy. TypeScript là ngôn ngữ phù hợp để phát triển JavaScript ở các dự án phát triển ứng dụng (application).

Điểm mạnh Điểm yếu
  • Mã an toàn và dễ đọc hơn.
  • Có hỗ trợ chức năng IntelliSense và IDE. 
  • Hỗ trợ trên nhiều trình duyệt khác nhau.
  • Không phù hợp với các dự án quy mô nhỏ. 
  • Cần thêm các bước xây dựng mã
  • Các cú pháp hoặc kiểu bổ sung có thể khiến mã phức tạp hơn.

Đọc thêm: TypeScript là gì: Tìm hiểu chi tiết về cách hoạt động của TypeScript

Câu hỏi phỏng vấn TypeScript cho Fresher

Các kiểu dữ liệu nguyên thủy trong TypeScript là gì?

TypeScript có các kiểu dữ liệu phổ biến thường được sử dụng là string, number và boolean. Ngoài ra còn nhiều kiểu dữ liệu khác. Chúng tương ứng với các kiểu dữ liệu trong JavaScript. Trong đó: 

  • string: Biểu diễn các giá trị văn bản như “javascript”, “typescript”,…
  • number: Biểu diễn các giá trị số như 1, 2, 32, 43,..
  • boolean: Biểu diễn một biến có thể có giá trị ‘true’ hoặc ‘false’

Giải thích cách mảng (array) hoạt động trong TypeScript

Bạn có thể sử dụng array để lưu trữ các giá trị cùng loại. Mảng là tập hợp các giá trị được sắp xếp và lập chỉ mục. Việc lập chỉ mục (index) bắt đầu từ 0, tức là phần tử đầu tiên có chỉ mục 0, phần tử thứ hai có chỉ mục 1.

let values: number[] = [];
values[0] = 10;
values[1] = 20;
values[2] = 30;

Bạn cũng có thể tạo một mảng bằng cú pháp viết tắt như sau:

let values: number[] = [15, 20, 25, 30];

Ngoài ra, TypeScript cung cấp cú pháp thay thế để chỉ định kiểu mảng (array):

let values: Array<number> = [15, 20, 25, 30];

Void là gì, và khi nào sử dụng kiểu void?

Trong TypeScript, “void” là kiểu dữ liệu dùng để chỉ ra rằng một hàm không trả về giá trị nào. Nó thường được sử dụng cho các hàm chỉ thực hiện tác vụ mà không cần trả về kết quả. Ví dụ:

function notify(): void {
  alert("The user has been notified.");
}

Không giống any (chấp nhận mọi kiểu), void biểu thị sự vắng mặt của giá trị trả về. Lưu ý, biến kiểu void ít được sử dụng trực tiếp, và theo cấu hình strictNullChecks, bạn chỉ có thể gán undefined cho nó trừ khi khai báo rõ ràng kiểu union như void | null.”

Bạn cũng có thể thêm ví dụ về hàm trả về undefined so sánh với void để làm rõ.

function returnsUndefined(): undefined {
  return undefined; // Phải trả về rõ ràng
}

Giải thích cú pháp hàm mũi tên (arrow function) trong TypeScript

Hàm mũi tên (Arrow function) trong TypeScript có cú pháp ngắn gọn hơn hàm thông thường.

Để hiểu hơn về arrow function, bạn có thể thực hiện một ví dụ nhỏ về một hàm thông thường cộng hai số và trả về một số.

function add(x: number, y: number): number {
let sum = x + y;
return sum;
}

Sau đó, sử dụng arrow function để có thể được định nghĩa như sau:

let add = (x: number, y: number): number => {
let sum = x + y;
return sum;
}

Bạn có thể đơn giản hóa cú pháp hơn nữa bằng cách loại bỏ dấu ngoặc và câu lệnh return. Điều này cho phép khi thân hàm chỉ bao gồm một câu lệnh. Ví dụ, nếu loại bỏ biến tổng tạm thời, có thể viết lại hàm trên như sau:

let add = (x: number, y: number): number => x + y;

Bạn hiểu gì về null và cách sử dụng chúng trong TypeScript

Trong lập trình, giá trị null biểu thị cho giá trị không tồn tại hoặc trống. Biến null không trỏ đến bất kỳ đối tượng nào. Do đó, bạn không thể truy cập bất kỳ thuộc tính nào trên biến hoặc gọi phương thức trên biến đó.

Trong TypeScript, giá trị null được chỉ ra bằng từ khóa ‘null’. Bạn có thể kiểm tra xem giá trị có phải là null hay không như sau:

function greet(name: string | null) {
if (name === null) {
  console.log("Name is not provided");
} else {
  console.log("Good morning, " + name.toUpperCase());
}
}

var foo = null;
greet(foo); // "Name is not provided"

foo = "Anders";
greet(foo);  // "Good morning, ANDERS"

Undefined trong TypeScript là gì?

undefined xuất hiện khi một biến được khai báo nhưng chưa được gán giá trị. undefined không đem lại hiệu quả cao nếu chỉ đứng một mình.

Một biến là undefined nếu nó được khai báo, nhưng không có giá trị nào được gán cho nó. Ngược lại, null được gán cho một biến và nó không biểu diễn giá trị nào.

console.log(null == null); // true
console.log(undefined == undefined); // true
console.log(null == undefined); // true, with type-conversion
console.log(null === undefined); // false, without type-conversion
console.log(0 == undefined); // false
console.log('' == undefined); // false
console.log(false == undefined); // false

Toán tử == cho phép ép kiểu nên null == undefined trả về true, trong khi === kiểm tra cả kiểu dữ liệu nên trả về false.

Toán tử typeof là gì? Sử dụng như thế nào trong TypeScript?

Tương tự như JavaScript, toán tử typeof trong TypeScript trả về kiểu của toán hạng dưới dạng chuỗi.

console.log(typeof 10);  // "number"

console.log(typeof 'foo');  // "string"

console.log(typeof false);  // "boolean"

console.log(typeof bar);  // "undefined"

Ngoài ra, trong TypeScript, bạn có thể sử dụng toán tử typeof trong ngữ cảnh để tham chiếu đến kiểu của thuộc tính hoặc biến.

let greeting = "hello";

let typeOfGreeting: typeof greeting;  // Tương đương với let typeOfGreeting : string

Cung cấp cú pháp cho tham số tùy chọn trong TypeScript

Một hàm có thể đánh dấu một hoặc nhiều tham số của nó là tùy chọn bằng cách thêm thêm dấu ? sau tên tham số. Trong ví dụ dưới đây, tham số greeting được đánh dấu là tùy chọn.

function greet(name: string, greeting?: string) {
if (!greeting)
  greeting = "Hello";

console.log(`${greeting}, ${name}`);
}

greet("John", "Hi");  // Hi, John
greet("Mary", "Hola");  // Hola, Mary
greet("Jane");  // Hello, Jane

Trong trường hợp này, nếu greeting không được cung cấp, nó sẽ là undefined và được gán giá trị mặc định là ‘Hello’.”*

Có thể thêm lưu ý về cách đặt giá trị mặc định trực tiếp bằng greeting: string = ‘Hello’ thay vì dùng if, như:

function greet(name: string, greeting: string = 'Hello') {  
  console.log(`${greeting}, ${name}`);  
}

Mục đích của tệp tsconfig.json là gì?

tsconfig.json là tệp cấu hình cho trình biên dịch TypeScript, chứa thông tin như đường dẫn tệp nguồn (include, exclude) hay cài đặt biên dịch (compilerOptions).

Ví dụ:

{
 "compilerOptions": {
   "module": "system",
   "noImplicitAny": true,
   "removeComments": true,
   "outFile": "../../built/local/tsc.js",
   "sourceMap": true
 },
 "include": ["src/**/*"],
 "exclude": ["node_modules", "**/*.spec.ts"]
}

Làm thế nào để kiểm tra null chặt chẽ trong TypeScript?

null là một trong những nguồn phổ biến nhất gây ra những lỗi runtime không mong muốn trong lập trình. TypeScript sẽ giúp tránh chúng bằng cách thực thi kiểm tra null nghiêm ngặt.

Bạn có thể thực thi kiểm tra null nghiêm ngặt theo hai cách:

  • Sử dụng tùy chọn –strictNullChecks cho trình biên dịch TypeScript (tsc)
  • Đặt thuộc tính strictNullChecks thành true trong tệp cấu hình tsconfig.json.

Khi tắt strictNullChecks TypeScript bỏ qua các giá trị null và undefined trong mã. Khi là true, null và undefined có các kiểu riêng biệt. Trình biên dịch sẽ đưa ra lỗi kiểu nếu bạn cố gắng sử dụng chúng ở nơi mong đợi một giá trị cụ thể.

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-la-gi-2

Nguồn @serokell.io

TypeScript là một ngôn ngữ nhập tĩnh (statical typed) 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 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.

Xem thêm: TypeScript vs JavaScript: Tính năng và trường hợp sử dụng

Lợi ích của việc sử dụng TypeScript là gì?

Tips: Mỗi ngôn ngữ lập trình đều có ưu – nhược điểm và lý do chúng được sử dụng. Câu hỏi này nhằm để hiểu rõ hơn về trải nghiệm cá nhân của dev khi sử dụng TypeScript, cũng như cách nó ảnh hưởng đến quy trình làm việc của dự án. Do đó, thay vì chỉ liệt kê các lợi ích của TypeScript, hãy dẫn chứng rằng một dự án điển hình có thể hưởng lợi gì từ nó, ví dụ như khả năng bảo trì tốt hơn trong thời gian dài, hoặc giúp developer tăng năng suất làm việc.

Trả lời: 

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 (và 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.

typescript-la-gi-3

Bảng tóm tắt ưu điểm và nhược điểm của TypeScript. Nguồn @appventurez

Interface trong TypeScript là gì?

Tips: Tương tự câu hỏi về tính năng, câu hỏi phỏng vấn TypeScript này kiểm tra độ hiểu biết và quen thuộc của ứng viên đối với ngôn ngữ. Một câu trả lời lý tưởng cần bao gồm cách sử dụng và lợi ích của interface trong TypeScript.

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 a 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.

Làm thế nào để tạo type mới bằng một tập hợp con của interface?

Tips: 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,
};

Các “enum” hoạt động như thế nào trong TypeScript?

Tips: 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. Bạn cần giải thích về enum ở cấp độ cao, đồng thời trình bày được các tính năng và cách sử dụng cơ bản của chúng.

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 type”

enum 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.

Hàm arrow (hàm mũi tên) trong TypeScript là gì?

Tips: 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,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

Sự khác nhau giữa var, let và const trong TypeScript là gì?

Tips: 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 it's 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

Khi nào bạn sử dụng return type là “never” và nó khác với “void” như thế nào?

Tips: Đây là câu hỏi thể hiện hiểu biết của bạn về các “type” trong TypeScript và cách sử dụng chúng trong việc xác định return type. Bạn cần phân biệt giữa hai type và xác định được return type của một hàm thông qua việc xem nội dung của nó.

Trước khi đi sâu vào sự khác biệt giữa nevervoid, 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 (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('Will');
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ề.

TypeScript hỗ trợ những Access modifier (phạm vi truy cập) nào?

Tips: Access modifier (phạm vi truy cập) và encapsulation (sự đóng gói) song hành với nhau. Câu hỏi phỏng vấn TypeScript 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 (lớp). 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, 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ó. 

Ví dụ: một biến hoặc phương thức với từ khóa protected sẽ có thể truy cập được từ bất kỳ đâu trong class của nó, đồng thời trong một class khác mở rộng class chứa biến hoặc phương thức đó.

  • 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, 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 {

  private username; // only accessible inside the `User` class

  // 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.

Generic là gì và cách sử dụng chúng trong TypeScript?

Tips: Bên cạnh ý “là gì”, người hỏi cũng có thể đang muốn biết các hàm generic liên quan đến tính linh hoạt của code thế nào, cũng như lý do đằng sau việc sử dụng generic thay vì các type khác. Hãy chia sẻ hiểu biết về generic là gì, cách hoạt động và lợi ích của nó.

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.

Câu hỏi phỏng vấn TypeScript cho Junior hoặc Senior Developer 

Bạn có thể giải thích type inference trong TypeScript là gì không?

Type inference là một tính năng trong TypeScript, trong đó trình biên dịch tự động sẽ xác định kiểu của biến dựa trên giá trị ban đầu của biến đó. Tính năng này giúp giảm việc sử dụng type annotations trong khi vẫn đảm bảo an toàn kiểu (type safety).

Ví dụ, nếu bạn khai báo một biến và gán một số cho biến đó, TypeScript sẽ suy ra kiểu của biến đó là ‘số’. Điều này đảm bảo rằng bạn không thể vô tình gán một giá trị có kiểu khác cho biến sau này.

Sự khác biệt chính giữa kiểu ‘any’ và ‘unknown’ trong TypeScript là gì?

Sự khác nhau cơ bản giữa any và unknown chính là: 

  • any cho phép bạn gán bất kỳ giá trị nào cho một biến mà không cần kiểm tra kiểu dữ liệu, về cơ bản là chọn không kiểm tra kiểu cho biến đó. 
  • unknown là phiên bản an toàn hơn của any. Bạn có thể gán bất kỳ giá trị nào cho unknown, nhưng phải kiểm tra kiểu dữ liệu trước, nếu không sẽ không thể thực hiện các thao tác tiếp theo.

Sử dụng ‘unknown’ khuyến khích tính an toàn của kiểu tốt hơn và giúp tránh lỗi runtime bằng cách buộc bạn phải thực hiện kiểm tra kiểu trước khi sử dụng biến.

TypeScript giúp phát hiện lỗi sớm hơn so với JavaScript như thế nào?

TypeScript là một ngôn ngữ có kiểu tĩnh (statically typed), nghĩa là nó kiểm tra lỗi kiểu tại thời điểm biên dịch, thay vì để đến runtime như JavaScript. Điều này cho phép phát hiện và sửa lỗi sớm trong quá trình phát triển, giảm nguy cơ lỗi khi ứng dụng chạy thực tế.

  • Không khớp kiểu: let num: number = ‘text’; (lỗi compile-time).
  • Thuộc tính không tồn tại: obj.name khi obj không có name.
  • Đối số sai: gọi fn(123) khi hàm yêu cầu chuỗi.

Trong JavaScript, những lỗi này chỉ lộ ra khi chạy mã, còn TypeScript báo lỗi ngay lập tức.

Bạn hiểu gì về ‘type guards’ trong TypeScript?

Type guards là các biểu thức trong TypeScript giúp kiểm tra kiểu dữ liệu của một biến trước khi sử dụng, giúp TypeScript hiểu rõ kiểu dữ liệu bên trong khối kiểm tra. Type guards sẽ giúp viết mã an toàn hơn về kiểu bằng cách thu hẹp kiểu của một biến trong một khối có điều kiện.

Type guards phổ biến bao gồm các toán tử ‘typeof’ và ‘instanceof’, kiểm tra kiểu của một biến và thể hiện của một đối tượng tương ứng.

Ví dụ:

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else {
    console.log(value.toFixed(2));
  }
}

Type assertion trong TypeScript là gì và khi nào nên sử dụng?

Type assertion là một cơ chế trong TypeScript cho phép bạn ghi đè kiểu suy ra và chỉ định một kiểu khác. Tương tự như ép kiểu trong các ngôn ngữ khác nhưng không thực hiện bất kỳ quy trình kiểm tra runtime hoặc chuyển đổi nào.

Bạn có thể sử dụng type assertion khi bạn chắc chắn về kiểu của một biến trong một ngữ cảnh cụ thể và muốn thông báo cho trình biên dịch TypeScript về điều đó. Ví dụ, khi xử lý các thư viện của bên thứ ba hoặc phân tích cú pháp JSON, khẳng định kiểu có thể hữu ích.

Readonly trong TypeScript hoạt động như thế nào?

‘readonly’ trong TypeScript được sử dụng để làm cho các thuộc tính của đối tượng không thể thay đổi. Khi một thuộc tính được đánh dấu là ‘readonly’, nó không thể được gán lại sau lần gán ban đầu.

Điều này hữu ích để tạo các cấu trúc dữ liệu không thể thay đổi và tăng cường độ tin cậy của mã bằng cách ngăn chặn các thay đổi không mong muốn.

TypeScript có hỗ trợ static class không? Nếu không, tại sao?

TypeScript không hỗ trợ khái niệm ‘static class’ (lớp mà tất cả thành phần đều là static) như trong C# hoặc Java. Trong các ngôn ngữ đó, mọi mã phải nằm trong class, và static class giúp định nghĩa hàm/dữ liệu không liên kết với đối tượng.

Ngược lại, TypeScript (cũng như JavaScript) cho phép khai báo hàm và dữ liệu độc lập mà không cần class, nên không cần ‘static class’. Tuy nhiên, TypeScript hỗ trợ thuộc tính và phương thức static trong class.

Ví dụ:

class Example {  
  static greet() { return 'Hello'; }  

console.log(Example.greet()); // Hello

Type Assertion trong TypeScript là gì?

Type Assertion trong TypeScript cho phép bạn nói với trình biên dịch rằng bạn biết chính xác kiểu dữ liệu của một biến tốt hơn TypeScript suy luận. Thông thường, điều này xảy ra khi bạn biết kiểu của một đối tượng cụ thể hơn kiểu hiện tại của nó. Trong những trường hợp như vậy, bạn có thể yêu cầu trình biên dịch TypeScript không suy ra kiểu của biến bằng cách sử dụng các khẳng định kiểu.

TypeScript cung cấp hai hình thức để khẳng định các kiểu:

cú pháp as

let value: unknown = "Foo";

let len: number = (value as string).length;

cú pháp <>

let value: unknown = "Foo";

let len: number = (<string>value).length;

Type Assertion tương tự như các kiểu trong các ngôn ngữ lập trình khác như C# hoặc Java. Tuy nhiên, không giống như các ngôn ngữ đó, không có hình phạt thời gian chạy nào đối với việc đóng hộp và mở hộp các biến để phù hợp với các kiểu. Khẳng định kiểu chỉ đơn giản cho trình biên dịch TypeScript biết kiểu của biến.

Type Alias trong TypeScript là gì? Làm thế nào để tạo Type Alias?

Type Alias cho phép bạn đặt tên mới cho một kiểu dữ liệu hiện có, giúp mã dễ đọc hơn và tránh lặp lại khi sử dụng nhiều lần. Chúng không tạo ra kiểu mới mà tạo ra tên mới tham chiếu đến kiểu đó.

Ví dụ, bạn có thể đặt tên mới cho một kiểu hợp nhất để tránh nhập tất cả các kiểu ở mọi nơi mà giá trị đang được sử dụng.

type alphanumeric = string | number;

let value: alphanumeric = "";

value = 10;

Khác với interface, type alias không thể mở rộng bằng extends nhưng linh hoạt hơn với union/intersection.

Làm thế nào để biến thuộc tính của đối tượng thành bất biến (immutable) trong TypeScript?

Bạn có thể đánh dấu các thuộc tính đối tượng là không thay đổi bằng cách sử dụng từ khóa readonly trước tên thuộc tính. Ví dụ:

interface Coordinate {

readonly x: number;

readonly y: number;

}

Khi bạn đánh dấu một thuộc tính là chỉ đọc, nó chỉ có thể được thiết lập khi bạn khởi tạo đối tượng. Sau khi đối tượng được tạo, bạn không thể thay đổi nó.

let c: Coordinate = { x: 5, y: 15 };

c.x = 20; // Cannot assign to 'x' because it is a read-only property.(2540)

Mục đích của toán tử ‘in’ trong TypeScript là gì?

Toán tử “in” được sử dụng để tìm xem một thuộc tính có nằm trong đối tượng được chỉ định hay không. “in” sẽ trả về giá trị true nếu thuộc tính đó thuộc về đối tượng. Nếu không, sẽ trả về false.

const car = { make: 'Mercedes', model: 'S450', year: 2023 };

console.log('model' in car);  // true

console.log('test' in car);  // false

Nó cũng hoạt động với thuộc tính kế thừa:

class Car { year = 2023; }  

const c = new Car();  

console.log('year' in c); // true

Khái niệm kế thừa (inheritance) trong TypeScript là gì?

Kế thừa (inheritance) cho phép bạn tạo ra một class mới dựa trên class hiện có bằng cách sử dụng từ khóa “extends”. Lớp kế thừa một lớp khác được gọi là lớp dẫn xuất (the derived class) và lớp được kế thừa được gọi là lớp cơ sở (the base class).

Trong TypeScript, một lớp chỉ có thể mở rộng một lớp. TypeScript sử dụng từ khóa ‘extends’ để chỉ định mối quan hệ giữa lớp cơ sở và các lớp dẫn xuất.

class Rectangle {
length: number;
breadth: number

constructor(length: number, breadth: number) {
  this.length = length;
  this.breadth = breadth
}

area(): number {
  return this.length * this.breadth;
}
}

class Square extends Rectangle {
constructor(side: number) {
  super(side, side);
}

volume() {
  return "Square doesn't have a volume!"
}
}

const sq = new Square(10);

console.log(sq.area());  // 100
console.log(sq.volume());  // "Square doesn't have a volume!"

Ở ví dụ trên, vì Square class mở rộng chức năng từ Rectangle class nên có thể tạo instance của square và gọi cả hai bằng phương thức area() và volume().

Câu hỏi phỏng vấn TypeScript về type inference 

Type inference hoạt động như thế nào trong TypeScript và lợi ích của chúng?

Type inference trong TypeScript là quá trình mà TypeScript tự động suy luận kiểu dữ liệu của biến hoặc hàm dựa trên giá trị được gán ban đầu mà không cần khai báo kiểu rõ ràng.

Những lợi ích của Type inference bao gồm:

  • Giúp mã ngắn gọn, dễ đọc hơn mà vẫn đảm bảo an toàn kiểu dữ liệu.
  • Giúp tăng hiệu suất làm việc khi không phải khai báo kiểu dữ liệu thủ công cho mọi biến.
  • Duy trì tính an toàn của kiểu trong khi cho phép phong cách mã hóa giống JavaScript hơn. 

Giải thích cách type inference hoạt động với khai báo const trong TypeScript?

Khi sử dụng const để khai báo một biến trong TypeScript, type inference sẽ là hằng số không thay đổi. Thay vì suy luận một kiểu chung, nó suy luận một kiểu theo nghĩa đen dựa trên giá trị được gán.

Ví dụ:

  • const x = ‘hello’ sẽ suy luận kiểu là ‘hello’ theo nghĩa đen thay vì chỉ là chuỗi (string).
  • const arr = [1, 2, 3] sẽ suy luận là number[], nhưng độ dài cũng được coi là một phần của kiểu.

TypeScript suy luận kiểu trả về cho hàm như thế nào?

TypeScript suy ra các kiểu trả về của hàm dựa trên giá trị được trả về trong hàm. Nghĩa là sẽ kiểm tra tất cả các câu lệnh trả về trong hàm và xác định kiểu phù hợp nhất bao gồm tất cả các giá trị trả về có thể.

Ví dụ:

  • Nếu một hàm luôn trả về số, TypeScript sẽ suy ra kiểu trả về là số.
  • Nếu một hàm có thể trả về chuỗi hoặc null, TypeScript sẽ suy ra kiểu hợp nhất string | null.

Contextual typing trong TypeScript là gì và nó liên quan gì đến type inference?

Contextual typing là một dạng suy luận kiểu trong TypeScript, trong đó TypeScript sẽ suy luận kiểu dựa trên ngữ cảnh sử dụng biến hoặc hàm. Điều này thường xảy ra với các đối số hàm, trong đó kiểu mong đợi được biết từ khai báo của hàm.

Ví dụ, trong trình xử lý sự kiện hoặc hàm gọi lại (callback function), TypeScript có thể suy ra kiểu của đối tượng sự kiện hoặc tham số gọi lại dựa trên cách chúng được sử dụng. Điều này cho phép kiểm tra kiểu tốt hơn mà không cần chú thích rõ ràng.

Ngoài ra, bạn có thể đưa thêm ví dụ về trường hợp sử dụng contextual typing theo ngữ cảnh, chẳng hạn như trong các phương thức mảng như map hoặc filter. 

Type widening hoạt động như thế nào trong TypeScript và khi nào nó xảy ra?

Type widening là một quá trình TypeScript suy luận kiểu dữ liệu rộng hơn khi không có ràng buộc cụ thể. Điều này thường xảy ra với các khai báo let khi giá trị ban đầu không biểu diễn đầy đủ tất cả các giá trị có thể có trong tương lai.

Ví dụ:

  • let x = 3 suy ra x là số, không phải là số 3 theo nghĩa đen
  • let y = null suy ra y là any, không phải là null. Nếu tắt strictNullChecks, nó vẫn là null, không phải any.

TypeScript suy luận kiểu trong array và object destructuring như thế nào?

Trong mảng và object destructuring, TypeScript suy ra các kiểu dựa trên cấu trúc và kiểu của mảng hoặc đối tượng. 

  • Đối với mảng, TypeScript suy luận kiểu dựa trên thứ tự phần tử.
  • Đối với object, TypeScript suy luận kiểu dựa trên tên thuộc tính.

Ví dụ:

  • Trong const [first, second] = [1, ‘two’], TypeScript suy ra first là số và second là chuỗi.
  • Trong const {name, age} = person, các kiểu được suy ra dựa trên các thuộc tính của đối tượng person. 

TypeScript xử lý type inference trong generic function như thế nào?

Trong generics, TypeScript suy luận kiểu dựa trên tham số truyền vào khi gọi hàm. Điều này cho phép mã linh hoạt và có thể tái sử dụng trong khi vẫn duy trì được tính an toàn của kiểu.

Ví dụ, trong một hàm:

function identity<T>(arg: T): T

TypeScript sẽ suy ra kiểu T dựa trên đối số được truyền vào. Nếu bạn gọi identity(42), T được suy ra là số.

noImplicitAny trong TypeScript là gì và ảnh hưởng như thế nào đến type inference?

Tùy chọn trình biên dịch ‘noImplicitAny’ trong TypeScript ngăn trình biên dịch suy ra kiểu any khi không thể xác định kiểu cụ thể hơn. Khi tùy chọn này được bật, TypeScript sẽ đưa ra lỗi thay vì sử dụng any một cách âm thầm.

function log(x) { console.log(x); } // Lỗi với noImplicitAny: x implicitly has an 'any' type  

function log(x: number) { console.log(x); } // OK 

Tùy chọn này khuyến khích nhập rõ ràng hơn và có thể giúp phát hiện sớm các sự cố liên quan đến kiểu tiềm ẩn. Điều này đặc biệt hữu ích trong các tình huống mà suy luận kiểu quá dễ dàng, chẳng hạn như với các tham số hàm không có giá trị mặc định.

Câu hỏi phỏng vấn TypeScript xử lý tình huống 

Bạn sẽ xử lý tình huống phải làm việc với object hoặc biến có thể null trong TypeScript như thế nào?

Khi làm việc với đối tượng hoặc biến có thể null trong TypeScript, trước tiên, tôi sẽ sử dụng toán tử Optional Chaining (?.) để kiểm tra sự tồn tại của thuộc tính trước khi truy cập, nhằm tránh lỗi TypeError khi truy cập vào thuộc tính của null hoặc undefined. Ví dụ:

const value = obj?.property?.subProperty;

Ngoài ra, tôi có thể sử dụng toán tử Nullish Coalescing (??) để cung cấp giá trị mặc định nếu biến có giá trị null hoặc undefined, đảm bảo ứng dụng hoạt động ổn định:

const name = user.name ?? 'Unknown';

Để tăng cường kiểm tra kiểu, có thể bật strictNullChecks trong tsconfig.json, giúp TypeScript phát hiện các lỗi liên quan đến null và undefined ngay trong quá trình biên dịch.

Ngoài ra, bạn nên giải thích cách sử dụng Optional Chaining và Nullish Coalescing để thể hiện khả năng kiểm soát dữ liệu nullable trong TypeScript một cách an toàn và hiệu quả. Đồng thời, việc đề cập đến cờ strictNullChecks cho thấy bạn hiểu sâu về cách TypeScript xử lý các kiểu nullable.

Bạn sẽ sử dụng chiến lược nào để mở rộng và tái sử dụng interfaces để đảm bảo tính linh hoạt trong dự án TypeScript?

Để mở rộng và tái sử dụng interface trong TypeScript, bạn có thể sử dụng kỹ thuật kế thừa bằng từ khóa extends, giúp chia sẻ thuộc tính chung giữa các interface và tránh lặp lại mã nguồn. Ví dụ:

interface Person {
  name: string;
  age: number;
}

interface Employee extends Person {
  employeeId: string;
}

Bạn cũng có thể sử dụng Interface Merging khi làm việc với các thư viện bên thứ ba, giúp mở rộng kiểu của chúng một cách linh hoạt mà không cần phải chỉnh sửa trực tiếp mã nguồn của thư viện.

interface Lib { foo(): void; }  

interface Lib { bar(): void; } // Gộp thành { foo(): void; bar(): void; }

Hoặc để tăng tính linh hoạt, có thể sử dụng Generics khi định nghĩa interface, cho phép tái sử dụng interface cho nhiều loại dữ liệu khác nhau mà không cần phải định nghĩa lại.

interface Container<T> { value: T; }  

const numContainer: Container<number> = { value: 42 }; 

Bạn có thể giải thích cách thiết kế hệ thống phức tạp bằng TypeScript để đảm bảo an toàn kiểu dữ liệu không?

Khi thiết kế một hệ thống phức tạp bằng TypeScript, trước tiên tôi sẽ xây dựng các mô hình dữ liệu bằng cách sử dụng interface và type alias để đảm bảo an toàn kiểu dữ liệu và dễ bảo trì. Tôi sẽ chia nhỏ mô hình thành các phần có thể tái sử dụng và mở rộng được.

Tôi cũng sẽ sử dụng Generics để tạo ra các hàm và class linh hoạt nhưng vẫn đảm bảo kiểm tra kiểu chặt chẽ. Ví dụ:

class DataService<T> {
  constructor(private data: T[]) {}

  getAll(): T[] {
    return this.data;
  }
}

Để tăng cường an toàn kiểu dữ liệu, tôi sẽ bật các tùy chọn kiểm tra chặt chẽ trong tsconfig.json, như strictNullChecks hay noImplicitAny, giúp phát hiện lỗi từ sớm trong quá trình biên dịch.

Ngoài ra, tôi sẽ áp dụng các Design Pattern như Dependency Injection kết hợp với TypeScript Decorators để quản lý phụ thuộc và mở rộng tính năng linh hoạt hơn.

Bạn nên trình bày chi tiết cách tổ chức mô hình dữ liệu, sử dụng Generics và áp dụng các Design Pattern phù hợp để thể hiện khả năng thiết kế hệ thống phức tạp nhưng vẫn đảm bảo an toàn kiểu dữ liệu trong TypeScript.

Tổng kết về câu hỏi phỏng vấn TypeScript 

Câu hỏi phỏng vấn TypeScript có thể bao gồm từ kiến thức cơ bản về cú pháp đến các khái niệm về type, inference hay các câu hỏi xử lý tình huống. Việc chuẩn bị kỹ càng sẽ giúp bạn tự tin và tạo ấn tượng mạnh mẽ trước nhà tuyển dụng. Hãy thường xuyên cập nhật kiến thức mới để có sự chuẩn bị tốt nhất trong buổi phỏng vấn sắp tới nhé. Chúc bạn thành công!