Interface trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu về interface trong TypeScript, cách định nghĩa, cách mở rộng interface, và cách sử dụng chúng để thực thi kiểm tra kiểu dữ liệu.

Giới thiệu về interface trong TypeScript

Interface trong TypeScript định nghĩa các hợp đồng trong code của bạn. Chúng cũng cung cấp tên rõ ràng để kiểm tra kiểu dữ liệu.

Hãy bắt đầu với một ví dụ đơn giản:

function getFullName(person: {
    firstName: string;
    lastName: string
}) {
    return `${person.firstName} ${person.lastName}`;
}

let person = {
    firstName: 'John',
    lastName: 'Doe'
};

console.log(getFullName(person));

Đầu ra:

John Doe

Trong ví dụ này, trình biên dịch TypeScript kiểm tra đối số mà bạn truyền vào hàm getFullName().

Nếu đối số có hai thuộc tính có kiểu là chuỗi, thì sẽ vượt qua kiểm tra của trình biên dịch TypeScript. Nếu không, nó sẽ xảy ra lỗi.

Như bạn có thể thấy rõ trong đoạn code trên, chú thích kiểu của đối số hàm làm cho mã khó đọc.

Để giải quyết vấn đề này, TypeScript đưa ra khái niệm interface.

Phần sau định nghĩa một interface tên là Person có hai thuộc tính chuỗi:

interface Person {
    firstName: string;
    lastName: string;
}

Theo quy ước, tên interface theo camel case - viết hoa chữ cái đầu tiên. Ví dụ, Person, UserProfileFullName.

Sau khi định nghĩa interface Person, bạn có thể sử dụng nó như một kiểu dữ liệu. Và bạn có thể chú thích tham số hàm với tên interface:

function getFullName(person: Person) {
    return `${person.firstName} ${person.lastName}`;
}

let john = {
    firstName: 'John',
    lastName: 'Doe'
};

console.log(getFullName(john));

Mã bây giờ đã dễ đọc hơn trước.

Hàm getFullName() sẽ chấp nhận bất cứ đối số nào có hai thuộc tính kiểu chuỗi. Và nó không nhất thiết phải có chính xác hai thuộc tính kiểu chuỗi. Xem ví dụ sau:

Đoạn mã sau khai báo một đối tượng có bốn thuộc tính:

let jane = {
   firstName: 'Jane',
   middleName: 'K.'
   lastName: 'Doe',
   age: 22
};

Vì đối tượng jane có hai thuộc tính kiểu chuỗi là firstNamelastName, bạn có thể truyền nó vào hàm getFullName() như sau:

let fullName = getFullName(jane);
console.log(fullName); // Jane Doe

Thuộc tính tùy chọn (optional property)

Interface có thể có các thuộc tính tùy chọn. Để khai báo một thuộc tính tùy chọn, bạn sử dụng dấu chấm hỏi (?) ở cuối tên thuộc tính như sau:

interface Person {
    firstName: string;
    middleName?: string;
    lastName: string;
}

Trong ví dụ này, interface Person có hai thuộc tính bắt buộc và một thuộc tính tùy chọn.

Và sau đây là cách sử dụng interface Person trong hàm getFullName():

function getFullName(person: Person) {
    if (person.middleName) {
        return `${person.firstName} ${person.middleName} ${person.lastName}`;
    }
    return `${person.firstName} ${person.lastName}`;
}

Thuộc tính chỉ đọc (readonly property)

Nếu muốn thuộc tính chỉ có thể sửa đổi khi đối tượng được tạo lần đầu tiên, bạn có thể sử dụng từ khóa readonly trước tên của thuộc tính:

interface Person {
    readonly ssn: string;
    firstName: string;
    lastName: string;    
}

let person: Person;
person = {
    ssn: '171-28-0926',
    firstName: 'John',
    lastName: 'Doe'
}

Trong ví dụ này, thuộc tính ssn không thể thay đổi:

person.ssn = '171-28-0000';

Lỗi:

error TS2540: Cannot assign to 'ssn' because it is a read-only property.

Kiểu hàm (function type)

Ngoài việc mô tả một đối tượng với các thuộc tính, interface cũng cho phép bạn mô tả các kiểu hàm.

Để mô tả một kiểu hàm, bạn gán chữ ký hàm chứa danh sách tham số với các kiểu và kiểu trả về cho interface. Ví dụ:

interface StringFormat {
    (str: string, isUpper: boolean): string
}

Bây giờ, bạn có thể sử dụng interface kiểu hàm này.

Ví dụ sau đây minh họa cách khai báo một biến của một kiểu hàm và gán cho nó một giá trị hàm cùng kiểu:

let format: StringFormat;

format = function (str: string, isUpper: boolean) {
    return isUpper ? str.toLocaleUpperCase() : str.toLocaleLowerCase();
};

console.log(format('hi', true));

Đầu ra:

HI
Lưu ý rằng tên tham số không cần phải khớp với chữ ký hàm.

Ví dụ sau tương đương với ví dụ trên:

let format: StringFormat;

format = function (src: string, upper: boolean) {
    return upper ? src.toLocaleUpperCase() : src.toLocaleLowerCase();
};

console.log(format('hi', true));

Interface StringFormat đảm bảo lời gọi hàm cần phải truyền các đối số cần thiết: một kiểu string và một kiểu boolean.

Đoạn mã sau cũng hoạt động hoàn toàn tốt mặc dù lowerCase được gán cho một hàm không có đối số thứ hai:

let lowerCase: StringFormat;
lowerCase = function (str: string) {
    return str.toLowerCase();
}

console.log(lowerCase('Hi', false));
Lưu ý rằng đối số thứ hai được truyền khi hàm lowerCase() được gọi.

Triển khai interface

Nếu bạn đã làm việc với Java hoặc C#, bạn có thể thấy rằng công dụng chính của interface là định nghĩa hợp đồng giữa các lớp không liên quan.

Ví dụ: interface Json sau có thể được triển khai bởi bất kỳ lớp nào không liên quan:

interface Json {
   toJSON(): string
}

Sau đây khai báo một lớp thực thi interface Json:

class Person implements Json {
    constructor(private firstName: string,
        private lastName: string) {
    }
    toJson(): string {
        return JSON.stringify(this);
    }
}

Trong lớp Person, chúng tôi đã triển khai phương thức toJson() của interface Json.

Ví dụ sau cho thấy cách sử dụng lớp Person:

let person = new Person('John', 'Doe');
console.log(person.toJson());

Đầu ra:

{"firstName":"John","lastName":"Doe"}

Mở rộng interface trong TypeScript

Mở rộng interface từ một interface

Giả sử rằng bạn có một interface có tên là Mailable có chứa hai phương thức là send()queue() như sau:

interface Mailable {
    send(email: string): boolean
    queue(email: string): boolean
}

Và bạn có nhiều lớp đã triển khai interface Mailable.

Bây giờ, bạn muốn thêm một phương thức gửi email mới sau vào interface Mailable như thế này:

later(email: string, after: number): void

Tuy nhiên, việc thêm phương thức later() vào interface Mailable sẽ phá vỡ code hiện tại.

Để tránh điều này, bạn có thể tạo một interface mới mở rộng từ interface Mailable:

interface FutureMailable extends Mailable {
    later(email: string, after: number): boolean
}

Để mở rộng interface, bạn sử dụng từ khóa extends với cú pháp sau:

interface A {
    a(): void
}

interface B extends A {
    b(): void
}

Interface B mở rộng interface A, sau đó interface B có cả hai phương thức a()b().

Giống như class, interface FutureMailable kế thừa các phương thức send()queue() từ interface Mailable.

Sau đây là cách triển khai interface FutureMailable:

class Mail implements FutureMailable {
    later(email: string, after: number): boolean {
        console.log(`Send email to ${email} in ${after} ms.`);
        return true;
    }
    send(email: string): boolean {
        console.log(`Sent email to ${email} after ${after} ms. `);
        return true;
    }
    queue(email: string): boolean {
        console.log(`Queue an email to ${email}.`);
        return true;
    }
}

Mở rộng interface từ nhiều interface

Một interface có thể mở rộng từ nhiều interface, tạo ra sự kết hợp của tất cả các interface. Ví dụ:

interface C {
    c(): void
}

interface D extends B, C {
    d(): void
}

Trong ví dụ này, interface D mở rộng các interface B và interface C. Vì vậy, interface D có tất cả các phương thức của interface Bvà interface C, đó là phương thức a(), b()c().

Mở rộng interface từ một class

TypeScript cho phép một interface mở rộng một class. Trong trường hợp này, interface kế thừa các thuộc tính và phương thức của class.

Mở rộng interface từ một class chỉ có trong TypeScript, điều này hoàn toàn không có trong Java và C#.

Ngoài ra, interface có thể kế thừa các thành viên private và protected của class, chứ không chỉ các thành viên public.

Nó có nghĩa là khi một interface mở rộng một lớp với các thành viên private hoặc protected, interface chỉ có thể được thực hiện bởi lớp đó hoặc các lớp con của lớp mà từ đó interface mở rộng.

Bằng cách làm này, bạn hạn chế việc sử dụng interface chỉ đối với lớp hoặc các lớp con của lớp mà từ đó interface mở rộng. Nếu bạn cố gắng triển khai interface từ một lớp không phải là lớp con của lớp mà giao diện được kế thừa, bạn sẽ gặp lỗi. Ví dụ:

class Control {
    private state: boolean;
}

interface StatefulControl extends Control {
    enable(): void
}

class Button extends Control implements StatefulControl {
    enable() { }
}
class TextBox extends Control implements StatefulControl {
    enable() { }
}
class Label extends Control { }


// Error: cannot implement
class Chart implements StatefulControl {
    enable() { }
}

Tóm lược

  • Interface trong TypeScript định nghĩa các hợp đồng trong mã của bạn và cung cấp các tên rõ ràng để kiểm tra kiểu dữ liệu.
  • Interface có thể có các thuộc tính tùy chọn hoặc các thuộc tính chỉ đọc.
  • Interface có thể được sử dụng như các kiểu hàm.
  • Interface thường được sử dụng như các hợp đồng giữa các lớp không liên quan.
  • Interface có thể mở rộng một hoặc nhiều interface hiện có.
  • Một interface cũng có thể mở rộng một class. Nếu class chứa các thành viên private hoặc protected, interface chỉ có thể được thực hiện bởi class hoặc các class con của class đó.
TypeScript
Bài Viết Liên Quan:
Generic type trong TypeScript
Trung Nguyen 20/12/2020
Generic type trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu về generic type trong TypeScript cho phép bạn sử dụng các kiểu dữ liệu làm tham số chính thức.

Kiểu dữ liệu nâng cao trong TypeScript
Trung Nguyen 08/12/2020
Kiểu dữ liệu nâng cao trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu về kiểu giao (intersection type), an toàn kiểu (type guard), ép kiểu (type casting) và xác nhận kiểu (type assertions) trong TypeScript .

Class trong TypeScript
Trung Nguyen 05/12/2020
Class trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu từ A-Z về class (lớp), abstract class, kiểm soát truy cập, thuộc tính chỉ đọc, kế thừa, ... trong TypeScript.

Function trong TypeScript
Trung Nguyen 03/12/2020
Function trong TypeScript

Trong hướng dẫn này, bạn sẽ tìm hiểu về function (hàm) trong TypeScript và cách sử dụng chú thích kiểu để thực thi kiểm tra kiểu dữ liệu cho function.