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
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 readonly
và const
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'
Vì '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() { }
}