Nếu như phần 1 đã giúp nắm vững những câu hỏi phỏng vấn cơ bản về HTML, CSS và JavaScript. Thì phần 2 này chúng ta sẽ tiếp tục khám phá một chủ đề không kém phần quan trọng trong lĩnh vực Frontend: TypeScript. TypeScript không chỉ là một ngôn ngữ lập trình bổ sung thêm tính năng cho JavaScript, mà nó còn là một công cụ mạnh mẽ giúp phát triển ứng dụng web một cách hiệu quả và an toàn hơn.
Hy vọng rằng những thông tin này sẽ giúp ích cho các trong việc chuẩn bị cho buổi phỏng vấn. Hãy cùng bắt đầu hành trình khám phá TypeScript nhé!

Bạn chưa xem các phần trước, có thể tham khảo tại đây:
Trọn bộ câu hỏi phỏng vấn Intern, Fresher cho Front-end thực tế tại doanh nghiệp (Phần 1)

1. abstract class trong TypeScript là gì?

Trả lời:

abstract class trong TypeScript là một lớp không thể được khởi tạo trực tiếp, nhưng có thể được sử dụng như là một lớp cơ sở cho các lớp khác. Abstract classes cung cấp một cách để định nghĩa các thuộc tính và phương thức chung có thể được chia sẻ giữa nhiều lớp, đồng thời vẫn cho phép tùy chỉnh và kế thừa.

2. Những điểm khác biệt chính giữa việc sử dụng abstract class và việc triển khai một interface và khi nào thì thích hợp để sử dụng mỗi loại trong dự án TypeScript?

Trả lời:

Abstract classesinterface đều được sử dụng trong lập trình hướng đối tượng để định nghĩa các điều kiện cho các lớp. Tuy nhiên, chúng phục vụ các mục đích khác nhau và có các trường hợp sử dụng khác nhau.

Abstract classes được sử dụng để cung cấp một triển khai cơ sở cho các lớp con. Abstract classes có thể chứa cả phương thức trừu tượng và phương thức cụ thể, nghĩa là chúng có thể định nghĩa một số phương thức với triển khai mặc định và để các phương thức khác cho các lớp con triển khai. Điều này cho phép áp đặt hành vi và cấu trúc chung trong các lớp con, trong khi vẫn cho phép tùy chỉnh.

Ngược lại, các interface được sử dụng để định nghĩa các điều kiện cho các lớp. Các interface chỉ có thể chứa các phương thức trừu tượng, nghĩa là chúng chỉ định nghĩa chữ ký của các phương thức mà không cung cấp bất kỳ triển khai nào. Các lớp thực hiện một interface phải cung cấp triển khai cho tất cả các phương thức của interface đó. Các interface được sử dụng để định nghĩa một tập hợp chung các thuộc tính và phương thức mà các lớp có thể triển khai.

Tóm lại, sẽ sử dụng một Abstract classes khi muốn cung cấp một triển khai chung cho các lớp con, trong khi sẽ sử dụng một interface khi muốn định nghĩa một tập hợp chung các thuộc tính và phương thức để các lớp triển khai.

Cũng cần lưu ý rằng TypeScript hỗ trợ đa kế thừa, nghĩa là một lớp có thể kế thừa từ nhiều interface, nhưng chỉ có thể kế thừa từ một lớp duy nhất.

3. Mục đích của việc sử dụng ký hiệu “?” trong TypeScript là gì, và nó chỉ ra điều gì về kiểu của biến?

Trả lời:

Ký hiệu "?" trong TypeScript chỉ ra rằng một biến có thể có giá trị hoặc có thể là null. Điều này được sử dụng để chỉ ra rằng một biến là tùy chọn, và nó là một cách để diễn đạt rằng một giá trị có thể không phải lúc nào cũng có mặt.

Ví dụ:

`interface` People {
   name: string;
   age?: number;
}

function add(a: number, b?: number): number {
   return a + (b ?? 0);
}

Trong ví dụ trên, thuộc tính age trong interface People là tùy chọn, nghĩa là nó có thể có hoặc không có giá trị. Tương tự, tham số b trong hàm add là tùy chọn, và nếu nó không được cung cấp, giá trị mặc định là 0 sẽ được sử dụng.

4. Sự khác biệt giữa kiểu “undefined” và “void” trong TypeScript là gì?

Trả lời:

Kiểu "undefined" trong TypeScript đại diện cho sự thiếu vắng của một giá trị, trong khi kiểu "void" được sử dụng để chỉ ra rằng một hàm không trả về giá trị. Nói cách khác, "undefined" được sử dụng để mô tả một giá trị không tồn tại, trong khi "void" được sử dụng để mô tả kiểu của một giá trị mà một hàm trả về khi nó không trả về gì cả.

Ví dụ:

function getData(): void {
    return 42; // lỗi: Kiểu 'number' không thể gán cho kiểu 'void'.
}

Trong ví dụ trên, hàm getData được khai báo với kiểu trả về là void, có nghĩa là nó không nên trả về bất kỳ giá trị nào. Việc cố gắng trả về một giá trị số sẽ gây ra lỗi biên dịch.

5. Kiểu “never” trong TypeScript là gì và khi nào nó được sử dụng?

Trả lời:

never là một kiểu không cho phép bất kỳ giá trị nào. Nó hữu ích khi được sử dụng với các điều kiện kiểm tra hết toàn bộ các trường hợp.

Ví dụ:

class Car {
  drive() {
    console.log("vroom");
  }
}
class Truck {
  tow() {
    console.log("dragging something");
  }
}
type Vehicle = Truck | Car;

let myVehicle: Vehicle = obtainRandomVehicle();

// Điều kiện kiểm tra hết toàn bộ các trường hợp
if (myVehicle instanceof Truck) {
  myVehicle.tow(); // Truck
} else if (myVehicle instanceof Car) {
  myVehicle.drive(); // Car
} else {
  // KHÔNG PHẢI CẢ HAI!
  const neverValue: never = myVehicle;
}

Trong ví dụ trên, kiểu never được sử dụng để đảm bảo rằng tất cả các trường hợp có thể của Vehicle đã được xử lý. Nếu myVehicle không phải là Truck hoặc Car, đoạn mã sẽ gây ra lỗi biên dịch, vì never không cho phép bất kỳ giá trị nào. Điều này giúp đảm bảo mã an toàn và không có lỗi logic.

6. Interface trong TypeScript là gì và được sử dụng như thế nào?

Trả lời:

Interface trong TypeScript là cách để mô tả cấu trúc của một đối tượng. Nó được sử dụng để định nghĩa các tên, kiểu dữ liệu và mối quan hệ giữa các thành phần của một đối tượng. Interface không chỉ đơn thuần mô tả cấu trúc mà còn giúp kiểm tra tính hợp lệ của các đối tượng trong quá trình phát triển và tránh các lỗi liên quan đến kiểu dữ liệu trong quá trình chạy chương trình.

7. Enum trong TypeScript là gì và được sử dụng như thế nào?

Trả lời:

Enum trong TypeScript là một cách để định nghĩa một nhóm các hằng số có tên. Nó được sử dụng để tạo ra một kiểu dữ liệu có một tập hợp cố định các giá trị có thể. Điều này hữu ích khi cần biểu diễn một tập hợp hữu hạn các lựa chọn, chẳng hạn như các ngày trong tuần hoặc các tháng trong năm.

Việc sử dụng enum giúp làm mã dễ đọc hơn và dễ bảo trì hơn bằng cách định nghĩa một tập hợp rõ ràng các giá trị có thể cho một kiểu dữ liệu cụ thể. Thay vì sử dụng các giá trị số hoặc chuỗi riêng lẻ, có thể sử dụng enum để chỉ ra một danh sách các lựa chọn có sẵn và mọi người đọc mã có thể hiểu được ý nghĩa của từng giá trị một cách dễ dàng.

Ví dụ:

// Định nghĩa một enum cho các tháng trong năm
enum Month {
    January = 1,
    February,
    March,
    April,
    May,
    June,
    July,
    August,
    September,
    October,
    November,
    December
}

// Sử dụng enum trong một hàm
function getMonthName(month: Month): string {
    switch (month) {
        case Month.January:
            return "January";
        case Month.February:
            return "February";
        case Month.March:
            return "March";
        case Month.April:
            return "April";
        case Month.May:
            return "May";
        case Month.June:
            return "June";
        case Month.July:
            return "July";
        case Month.August:
            return "August";
        case Month.September:
            return "September";
        case Month.October:
            return "October";
        case Month.November:
            return "November";
        case Month.December:
            return "December";
        default:
            throw new Error("Invalid month value");
    }
}

// Sử dụng hàm và enum
let currentMonth: Month = Month.July;
console.log(`The current month is: ${getMonthName(currentMonth)}`);

Trong ví dụ này, enumMonth định nghĩa một tập hợp các tháng trong năm từ January đến December. Mỗi giá trị trong enum có thể được gán một số để biểu diễn thứ tự của chúng. Khi sử dụng enum, có thể dễ dàng xác định một tháng cụ thể và làm cho mã trở nên dễ đọc và dễ hiểu hơn.

8. Type alias trong TypeScript là gì và được sử dụng như thế nào?

Trả lời:

Type alias trong TypeScript là cách để đặt tên cho một kiểu dữ liệu đã tồn tại hoặc một sự kết hợp của các kiểu dữ liệu. Nó cho phép tạo ra một tên viết tắt hoặc bí danh cho một kiểu dữ liệu phức tạp, từ đó giúp cho mã dễ đọc và dễ hiểu hơn. Type alias có thể được sử dụng để đơn giản hóa các kiểu dữ liệu phức tạp, cải thiện tính rõ ràng và khuyến khích sử dụng lại mã lệnh bằng cách định nghĩa các kiểu dữ liệu thường được sử dụng.

Ví dụ:

// Định nghĩa một type alias cho một đối tượng phức tạp
type User = {
    id: number;
    username: string;
    email: string;
    isAdmin: boolean;
};

// Sử dụng type alias
function getUserInfo(user: User): string {
    return `Người dùng ${user.username} (${user.email})`;
}

// Một ví dụ khác với union type
type Result = number | string;

function processResult(result: Result): void {
    if (typeof result === 'number') {
        console.log(`Kết quả là số: ${result}`);
    } else {
        console.log(`Kết quả là chuỗi: ${result}`);
    }
}

Trong ví dụ này, type alias User định nghĩa một đối tượng có các thuộc tính id, username, email, và isAdmin. Bằng cách sử dụng type alias, có thể dễ dàng sử dụng và hiểu rõ cấu trúc của đối tượng User trong toàn bộ mã. Ngoài ra, type alias Result dùng để biểu diễn một giá trị có thể là số hoặc chuỗi, giúp cho hàm processResult dễ dàng xử lý các kiểu dữ liệu khác nhau một cách linh hoạt.

9. Trong những tình huống nào sẽ sử dụng interface và type alias trong TypeScript?

Trả lời:

Trong nhiều tình huống, cả type aliasinterface đều có thể được sử dụng, tuy nhiên…

  • Nếu cần định nghĩa một kiểu dữ liệu không phải là kiểu đối tượng (ví dụ: sử dụng toán tử union |), phải sử dụng type alias.
  • Nếu cần định nghĩa một kiểu dữ liệu để sử dụng với từ khóa implements, thì tốt nhất là sử dụng interface.
  • Nếu cần cho phép người dùng mở rộng các kiểu dữ liệu, phải sử dụng interface.

Ví dụ 1: sử dụng type alias cho union type:

// Type alias cho union type
type Result = number | string;

function processResult(result: Result): void {
    if (typeof result === 'number') {
        console.log(`Kết quả là số: ${result}`);
    } else {
        console.log(`Kết quả là chuỗi: ${result}`);
    }
}

Ví dụ 2: sử dụng interface cho implements heritage term

// Interface cho implements heritage term
`interface` Shape {
    area(): number;
}

class Circle implements Shape {
    constructor(private radius: number) {}

    area(): number {
        return Math.PI * this.radius ** 2;
    }
}

10. Utility Types trong TypeScript có mục đích gì và làm thế nào chúng hỗ trợ trong việc biến đổi và tạo kiểu dữ liệu?

Trả lời:

Utility Types trong TypeScript là một bộ các phép toán kiểu dữ liệu tích hợp sẵn cho phép thao tác và tạo ra các kiểu dữ liệu mới dựa trên các kiểu hiện có. Chúng giúp tái sử dụng và tối ưu hóa mã một cách hiệu quả bằng cách định nghĩa các kiểu dữ liệu phức tạp hoặc điều chỉnh kiểu dữ liệu hiện có.

Các loại Utility phổ biến bao gồm:

  • Partial: Làm cho các thuộc tính của một kiểu trở thành optional.
  • Readonly: Làm cho các thuộc tính của một kiểu trở thành chỉ có thể đọc.
  • Required: Ngược lại của Partial, làm cho các thuộc tính trở thành bắt buộc.
  • Pick: Tạo ra một kiểu mới chỉ bao gồm các thuộc tính được chọn từ một kiểu hiện có.
  • Omit: Tạo ra một kiểu mới loại bỏ các thuộc tính được chỉ định từ một kiểu hiện có.
  • Exclude: Tạo ra một kiểu mới là phần bù của một kiểu giao (intersection type).
  • Record: Tạo ra một kiểu mới có tập hợp các thuộc tính với các khóa và giá trị cụ thể.
  • NonNullable: Tạo ra một kiểu mới loại bỏ null và undefined từ một kiểu dữ liệu.

Những Utility Type này giúp mô tả và xử lý dữ liệu một cách chính xác và rõ ràng hơn, từ đó giảm thiểu lỗi trong quá trình phát triển phần mềm và tăng tính bảo trì của mã nguồn. Chúng cũng cung cấp khả năng mở rộng mã nguồn thông qua việc định nghĩa và tái sử dụng các kiểu dữ liệu phức tạp một cách hiệu quả.

11. Generics trong TypeScript là gì và chúng hoạt động như thế nào?

Trả lời:

Generics trong TypeScript cho phép viết mã tái sử dụng và linh hoạt, có thể hoạt động với bất kỳ loại dữ liệu nào. Generics tương tự như các biến cho các kiểu dữ liệu. Khi định nghĩa một kiểu generic, có thể chỉ định loại dữ liệu mà hàm, lớp hoặc interface nên chấp nhận, và loại này có thể là bất kỳ loại dữ liệu hợp lệ nào như chuỗi (string), số (number), hoặc đối tượng (object). Generics được khai báo bằng dấu ngoặc nhọn (<>) và sau đó được sử dụng trong thực thi của mã.

**Ví dụ 1: **

function listToDict<T>(
  list: T[],
  idGen: (arg: T) => string
): { [k: string]: T } {
  const dict: { [k: string]: T } = {};
  list.forEach(item => {
    const key = idGen(item);
    dict[key] = item;
  });
  return dict;
}

Trong hàm listToDict, T là một generic type parameter. Nó cho phép hàm này hoạt động với một mảng các đối tượng của bất kỳ loại dữ liệu nào (T[]) và một hàm idGen nhận một đối tượng T và trả về một chuỗi. Hàm này trả về một đối tượng có các thuộc tính là các chuỗi được tạo bởi idGen, và giá trị của mỗi thuộc tính là các đối tượng từ mảng đầu vào.

Tổng kết

Chúng ta đã đi qua một số câu hỏi quan trọng mà các ứng viên Intern và Fresher có thể gặp phải trong quá trình phỏng vấn cho vị trí Front-end tại các doanh nghiệp. Việc chuẩn bị kỹ lưỡng và tự tin trước những câu hỏi này sẽ giúp nắm bắt cơ hội và tạo ấn tượng tích cực với nhà tuyển dụng. Như vậy, chúng ta đã khám phá sâu hơn về các chủ đề nâng cao trong TypeScript như Generics, Interface, Type Alias, EnumsUtility Types. Những kiến thức này không chỉ có tính lý thuyết mà còn rất ứng dụng trong thực tế, giúp xử lý các vấn đề phức tạp và viết mã tái sử dụng và linh hoạt hơn.

Đừng quên đồng hành cùng tôi để chuẩn bị tốt nhất cho cuộc phỏng vấn và tiến xa hơn trên con đường sự nghiệp Front-end của mình.

Hãy đón chờ Phần 3 sắp tới, nơi chúng ta sẽ khám phá thêm các chủ đề thú vị và chi tiết hơn. Đừng ngần ngại chia sẻ câu hỏi hoặc kinh nghiệm của bạn trong phần bình luận dưới đây. Cảm ơn bạn đã đọc và hẹn gặp lại trong bài viết tiếp theo!