Nội dung được giới thiệu trong bài viết này

Đây là bài viết thứ 3 trong loạt bài giới thiệu về TypeScript. Nối tiếp thành công vài bài viết thứ hai giới thiệu về Variable Declarations lần này mình sẽ tổng hộp lại Interfaces trong TypeScript

Our First Interface

Dưới đây là một số ví dụ về cách bạn có thể áp dụng Interfaces một các đơn giản:

Compiler sẽ kiểm tra xem trong tham số chuyền vào của hàm printLabel có thuộc tính label không. Những thuộc tính khác của Object nó sẽ bỏ qua.

TypeScript

function printLabel(labelledObj: { label: string }) {
    console.log(labelledObj.label);
}
;
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

Javascript

function printLabel(labelledObj) {
    console.log(labelledObj.label);
}
;
var myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

Thay vào đó giờ hãy thử sủ dụng Interfaces

TypeScript

interface LabelledValue {
    label: string;
}
;
function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
}
;
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

Javascript

function printLabel(labelledObj) {
    console.log(labelledObj.label);
}
;
var myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

Interfaces này chủ yểu để kiểm tra đầu vào. Sau khi compile nó sẽ bi xóa đi

Không cần quan tâm đến thứ tự thuộc tính khai báo trong interface.

Optional Properties

Đôi khi chúng ta không nhất thiết phải bắt buộc phải có tất cả thuộc tính vả để thể hiện những thuộc tính không bắt buộc cần thêm ? vào cuối biến đó.

TypeScript

interface SquareConfig {
    color?: string;
    width?: number;
}
;
function createSquare(config: SquareConfig): {color: string; area: number} {
    let newSquare = {color: "white", area: 100};
    if (config.color) {
        newSquare.color = config.color;
    }
    if (config.width) {
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}
;
let mySquare = createSquare({color: "black"});

Javascript

function createSquare(config) {
    var newSquare = { color: "white", area: 100 };
    if (config.color) {
        newSquare.color = config.color;
    }
    if (config.width) {
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}
;
var mySquare = createSquare({ color: "black" });

Mặc dù được chỉ định thuộc tính không bắt buộc trong Interface rồi nhưng JavaScript code được compile vẫn không thay đổi?

Điểm tiện lợi của việc định nghĩa tất cả các kiểu không bắt buộc là có thể check được có sự dụng nhầm biến không được khai báo không

interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
    let newSquare = {color: "white", area: 100};
    if (config.color) {
        // Lỗi. Biến 'clor' không tồn tại trong 'SquareConfig'
        newSquare.color = config.clor;
    }
    if (config.width) {
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}

let mySquare = createSquare({color: "black"});

Readonly Properties

Những thuộc tính mà chỉ dùng để đọc thì thêm readonly vào trước tên thuộc tính đó.

interface Point {
    readonly x: number;
    readonly y: number;
}

Trường hợp này có thể khởi tạo object chứa các giá trị thuộc tính của nó được nhưng không thể thay đổi

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // Lỗi!

TypeScript còn hỗ trợ class ReadonlyArray<T>. Class này giống với Array nhưng loại bỏ hết tất cả các hàm thay đổi Object

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // Lỗi!
ro.push(5); // Lỗi!
ro.length = 100; // Lỗi!
a = ro; // Lỗi!

Object của ReadonlyArray không thể thay đổi trẳng hạn như trong các thuật toán sắp xếp nhưng có thể sử dụng để ép kiểu.

a = ro as number[];

readonly vs const

Sử phân chia cách sử dụng của readonlyconst hết sức đơn giản. const sử dụng cho biến còn readonly sử dụng cho thuộc tính(property).

Excess Property Checks

Khi kết hợp giữa Interface và thuộc tính không bắt buộc có thể sẽ xảy ra một vài cạm bẫy như sau

interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
    // ...
}

let mySquare = createSquare({ colour: "red", width: 100 }); // :o định dùng là 'color' nhưng giờ bị nhầm thành 'colour'

'color' không bắt buộc nên trường hợp này Object truyền vào sẽ chỉ có thuộc tính là 'width'
Để tranh những nhầm lẫn này xẩy ra TypeScript sẽ thực hiện check riêng object được viết ra. Cụ thể khi gán cho biến khác hoặc khi đã chuyền tham số vào hàm sẽ xẩy ra lỗi không tồn tại thuộc tính có kiểu của đối tượng

// Lỗi, 'colour' không nằm trong 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });

Để bỏ qua check như trên chỉ cần ép kiểu là được

// 'opacity' không có trong 'SquareConfig' nhưng vẫn không vấn đề gì
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

Còn một cách làm tốt hơn nữa để nhận được thuộc tính ngoài là thêm index là một biến kiểu chuỗi như sau:

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}
  • Mặc dù đang được chỉ định thêm propName nhưng không hản có thể truy cập đến những thuộc tích khác nữa. Sẽ được nói rõ hơn ở phần Indexable Types phía dưới

Nếu chúng ta thi chỉ gán biến 1 lần sẽ chỉ mất 2 dòng code.

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

Có thể bạn nghĩ viết thế này đơn giản hơn rất nhiều nhưng khi code trở lên phức tạp hơn chúng ta cần phải sử dụng kỹ thật trên. Tức là trong nhiều trường hợp chúng ta phải ý thức trước được là sẽ có những thuộc tính dư thừa nữa.

Function Types

Interface ngoài việc thể hiện các Object có những thuộc tính đặc trung ra cỏn có thể thể hiện được cả method.
Dưới đây là ví dụ biểu thị khai báo một hàm bao gồm cả tham số chuyển vào và giá trị trả về. Danh sách tham số chuyền vào sẽ cần định nghĩa tên và kiểu dự liệu

interface SearchFunc {
    (source: string, subString: string): boolean;
}

Sau đó sẽ sử dụng sao cho giống với interface trên

let mySearch: SearchFunc;

mySearch = function(source: string, subString: string) {
    let result = source.search(subString);
    return result > -1;
}

Lúc này không cần phải để tên tham số chuyền vào chùng khớp với interface của hàm.

let mySearch: SearchFunc;

mySearch = function(src: string, sub: string): boolean {
    let result = src.search(sub);
    return result > -1;
}

Còn kiểu của tham số nếu không khai báo sẽ tự lấy kiểu theo như interface của hàm.

let mySearch: SearchFunc;

mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}
  • Mặc dù như vậy nhưng mình nghĩ định nghĩa kiểu tham số tại hàm càng nhiều càng tốt

Indexable Types

Trong TypeScript cũng thể hiện cả việc có thể truy cập vao mang như là a[10] hoặc ageMap["daniel"]. Indexble Types có 1 index signature mà nó có thể được định nghĩa kiểu và nó có thể được sử đụng trong Object

interface StringArray {
    [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

index signature được support 2 kiểu dữ liệu là kiểu chuỗi và kiểu số. Có thể hỗ trợ cả 2 loại này nhưng trường hợp dưới đây, kiểu giá trị trả về của index signature có kiểu số và kiểu giá trị tra về của index signature có kiểu chuỗi cần phỉ có subclass. Bởi vì trong javascript khi truy cập với index là kiếu số nó sẽ chuyển thành kiểu string

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

// Lỗi. Khi truy cập với index là kiểu chuỗi sẽ trả về Dog
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

Ngoài ra index signature cho phép đồng nhất về một kết quả

interface NumberDictionary {
    [index: string]: number;
    length: number;    // OK. length là kiểu số
    name: string;      // Lỗi. name không phải kiểu số cũng không phải subclass
}

Cuối cùng, bằng việc chỉ định readonly cho index signature sẽ không thể gán lại biến

interface ReadonlyStringArray {
    readonly [index: number]: string;
}

let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // Lỗi!

Class Types

Implementing an interface

Cũng giống ngôn ngữ khác sử dụng Interface cho class còn thông dụng hơn rất nhiều.

interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

Trong các class sẽ định nghĩa các hàm trong interface giống như trên đã nói.

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

Difference between the static and instance sides of classes

Trong khi sử dụng interface với class cần phải ý thức trước được rằng có 2 loại thành viên static va instance.
Như ví dụ dưới đây sẽ xẩy ra lỗi:

interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

Bởi vì khí thực hiện interface sẽ chỉ check những thành viên instance, nó sẽ bỏ qua thành viên static như là constructor.
Thay vào đó cần sử dụng interface vào trực tiếp các thanh phần static. Ví dụ dưới đây có ClockConstructor là interface riêng cho hàm khởi tạo

interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
    tick();
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

Extending Interfaces

Kế thừa của interface cũng giống như kế thừa của class

interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

Thậm chí có thể kế thừa nhiều interface

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

Hybrid Types

Trong interface, cho đến bây giờ có thể kết hợp nhiều cách sử dụng lại với nhau

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { }; //Interface chưa được thực hiện hến, chưa định nghĩa là 1 kiểu'Counter'
    counter.interval = 123;
    counter.reset = function () { };
    return counter; // 'Counter' Tất cả các thuộc tính và function đã được chuẩn bị rồi nên có thể trả về kiểu 'Counter'
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

Interfaces Extending Classes

Trường hợp interface đã kế thừa, thì trong class tất cả thành phần private/protected đêu được ghi đè.

Trong ví dụ dưới đây, class SelectableControl cũng sẽ chứa thuộc tính state, class Control lúc này sẽ bao gồm tất cả các thành phần. class Button (được kế thừa từ Control) sẽ chứa thuộc tính state và hàm select nên có thể xem như là class SelectableControl. Mặt khác, class Image chỉ chứa hàm select nên không thể coi như là class SelectableControl.

class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
}

class Button extends Control {
    select() { }
}

class Image {
    select() { }
}