openplanning

Constructor trong TypeScript

  1. Constructor là gì?
  2. Constructor
  3. Optional parameters
  4. Default Parameter values
  5. Parameter with Union Types
  6. Constructor Overloading
  7. Static factory method

1. Constructor là gì?

Constructor (phương thức khởi tạo) là một phương thức đặc biệt của lớp, được sử dụng để tạo ra đối tượng và khởi tạo các giá trị cho các trường (field).
Để đơn giản, chúng ta sẽ phân tích tình huống dưới đây:
Lớp Person được coi là một bản thiết kế để tạo ra các con người cụ thể. Nó bao gồm các thông tin: Tên, giới tính và quốc gia. Các thông tin này còn được gọi là các trường (field).
Đặc điểm của Constructor:
  • Constructor là một phương thức đặc biệt của lớp, từ khoá constructor cũng chính là tên của phương thức đặc biệt này.
  • Mỗi lớp chỉ có duy nhất một constructor.
  • Trong thân (body) của constructor bạn phải gán giá trị cho tất cả các trường (field) của lớp, các trường tuỳ chọn có thể không cần gán giá trị.
  • Nếu tất cả các trường (field) trong lớp là tuỳ chọn, và bạn không định nghĩa bất kỳ constructor nào cho một lớp, TypeScript sẽ tự động coi rằng lớp này có một constructor mặc định, không có tham số và không có nội dung trong thân.
Ngôn ngữ TypeScript được thiết kế chặt chẽ và an toàn, nó đòi hỏi tất các trường (field) của lớp phải được gán giá trị khác null. Nếu một một trường nào đó cho phép giá trị null bạn phải nói rõ điều đó trong thiết kế của lớp.

2. Constructor

Trong hầu hết các trường hợp sử dụng, dưới đây là cú pháp thông dụng để định nghĩa một constructor:
class Class_Name  {
   field_1 : data_type_f1;
   field_2 : data_type_f2;
   field_N : data_type_fN;

   constructor(arg_1 : data_type_a1, arg_2 : data_type_a2, arg_M : data_type_aM)  {
      // Codes..
   }
}
Trong ví dụ dưới đây, lớp Person có 3 trường, không có trường nào là tuỳ chọn, vì vậy trong thân của constructor bạn phải gán giá trị khác null cho tất cả các trường này.
constructor_ex1.ts
class Person {
    name: string;
    gender: string;
    country: string;

    constructor(n: string, g: string, c: string) {
        this.name = n;
        this.gender = g;
        this.country = c;
    }
    // A Method
    selfIntroduce(): void {
        console.log(`Hi, My name is ${this.name}, from ${this.country}`);
    }
}
function constructor_ex1_test() {
    var tom: Person = new Person("Tom", "Male", "USA");

    tom.selfIntroduce();
    console.log(`tom.name = ${tom.name}`);
}
constructor_ex1_test(); // Call the function.
Output:
Hi, My name is Tom, from USA
tom.name = Tom
Ví dụ:
constructor_ex2.ts
interface IDimension {
    width: number;
    height: number;
}
class Rectangle {
    width: number;
    height: number;
    color: string;

    constructor(dimension: IDimension, color: string) {
        this.width = dimension.width;
        this.height = dimension.height;
        this.color = color;
    }
    // A Method
    getArea(): number {
        return this.width * this.height;
    }
}
function constructor_ex2_test() {
    var dim: IDimension = { width: 10, height: 20 };
    var rec: Rectangle = new Rectangle(dim, "blue");

    console.log(`rec.getArea() = ${rec.getArea()}`);
    console.log(`rec.color = ${rec.color}`);
}
constructor_ex2_test(); // Call the function

3. Optional parameters

Sử dụng tham số tuỳ chọn trong một constructor là cách để bạn có thể sử dụng constructor này theo nhiều cách khác nhau. Chú ý: Các tham số tuỳ chọn phải là các tham số cuối cùng trong danh sách các tham số.
constructor(arg1 : data_type1, arg2 data_type2, arg3?: data_type3, arg4?: data_type4) {
   // code..
}
Ví dụ:
constructor_optional_args_ex1_test.ts
class Circle {
    radius: number;
    color: string;
  
    constructor(radius: number, color?: string) {
        this.radius = radius;
        this.color = color ?? "blue"; // Default value
    }
}
function constructor_optional_args_ex1_test() {
    var circle1 = new Circle(100);
    var circle2 = new Circle(100, "red");

    console.log(`circle1.color = ${circle1.color}`); // blue
    console.log(`circle2.color = ${circle2.color}`); // red
}
constructor_optional_args_ex1_test(); // Call the function.
Output:
circle1.color = blue
circle2.color = red

4. Default Parameter values

Sử dụng tham số với giá trị mặc định trong một constructor cũng là cách để bạn có thể sử dụng constructor này theo nhiều cách khác nhau. Chú ý: Các tham số này phải là các tham số cuối cùng trong trong danh sách các tham số.
constructor(arg1 : data_type1, arg2 : data_type2,
             arg3 : data_type3 = defaultValue3,
             arg4 : data_type4 = defaultValue4) {
   // code..
}
Ví dụ:
constructor_default_args_ex1.ts
class Square {
    width: number;
    color: string;
    shadows: boolean;

    constructor(width: number, color: string = "blue", shadows: boolean = true) {
        this.width = width;
        this.color = color;
        this.shadows = shadows;
    }
}
function constructor_default_args_ex1_test() {
    var square1 = new Square(100);
    var square2 = new Square(100, "red");
    var square3 = new Square(100, "red", false);

    console.log(`square1.color = ${square1.color}`); // blue
    console.log(`square1.shadows = ${square1.shadows}`); // true

    console.log(`square2.color = ${square2.color}`); // red
    console.log(`square2.shadows = ${square2.shadows}`); // true

    console.log(`square3.color = ${square3.color}`); // red
    console.log(`square3.shadows = ${square3.shadows}`); // false
}
constructor_default_args_ex1_test(); // Call the function.
Output:
square1.color = blue
square1.shadows = true
square2.color = red
square2.shadows = true
square3.color = red
square3.shadows = false

5. Parameter with Union Types

Tham số trong TypeScrpit có thể được khai báo với kiểu dữ liệu liên hợp (union data type), đây cũng là một kỹ thuật để bạn có thể sử dụng constructor theo nhiều cách khác nhau.
constructor(arg1 : data_type1,
             arg2 : data_type21 | data_type22 | data_type23,
             arg3 : data_type3,
             arg4 : data_type4) {
   // code..
}
Ví dụ:
constructor_union_type_args_ex1.ts
class Employee {
    name: string;
    hireDate: Date;
    department: string;
  
    constructor(name: string, hireDate: Date | string, department: string) {
        this.name = name;
        this.department = department;
        if(hireDate instanceof Date)  {
            this.hireDate = hireDate;
        } else {
            this.hireDate = new Date(hireDate);
        }
    }
}
function constructor_union_type_args_ex1_test() {
    var tom = new Employee("Tom", new Date("1995-05-01"), "IT");
    var jerry = new Employee("Jerry", "2001-05-01", "Operations");

    console.log(`tom.hireDate = ${tom.hireDate}`);  
    console.log(`jerry.hireDate = ${jerry.hireDate}`);  
} 
constructor_union_type_args_ex1_test(); // Call the function.
Output:
tom.hireDate = Mon May 01 1995 06:00:00 GMT+0600 (GMT+06:00)
jerry.hireDate = Tue May 01 2001 06:00:00 GMT+0600 (GMT+06:00)

6. Constructor Overloading

Như đã đề cập ở trên, TypeScript chỉ cho phép duy nhất một constructor trong một lớp. Constructor Overloading (nạp chồng phương thức khởi tạo) là một kỹ thuật "lách luật" trên. Nghĩa là bạn vẫn chỉ có một constructor trong một lớp nhưng có thể sử dụng nó với nhiều kiểu tham số khác nhau.
Trong TypeScript, Constructor Overloading trông khác với trong C++, Java hoặc C#. Ý tưởng chính để nạp chồng (overload) cho constructor là tạo một constructor chung để kiểm tra loại tham số nào đã được truyền để tạo một đối tượng và sau đó thực hiện một số logic cho trường hợp thích hợp. Hữu ích là thêm các định nghĩa cho constructor để giúp các lập trình viên khác biết cách sử dụng lớp một cách thích hợp.
Cú pháp:
constructor(arg_11 : data_type_11, arg_1N : data_type_1N); // Definition 1
constructor(arg_21 : data_type_21, arg_22 : data_type_22, arg_2M : data_type_2M); // Definition 2
constructor(... args : any[]) {
   // Constructor body.
}
  • TypeScript any data type
Về cơ bản, có rất nhiều cách để định nghĩa một constructor nạp chồng. Cú pháp trên được khuyến khích sử dụng, nó giúp bạn tránh được thông báo lỗi giống như dưới đây từ trình biên dịch:
This overload signature is not compatible with its implementation signature.
Constructor overloading example 1:
constructor_overload_ex1.js
interface IDimension3D {
    width: number;
    height: number;
    depth: number;
}
class Box {
    width: number;
    height: number;
    depth: number;
    color: string;

    constructor(dimension: IDimension3D, color?: string); // definition 1
    constructor(width: number, height: number, depth: number, color?: string); // definition 2
    constructor(...args: any[]) {
        if (args.length == 1 || args.length == 2) {  // Use definition 1
            this.width = args[0].width;
            this.height = args[0].height;
            this.depth = args[0].depth;
            this.color = args.length == 1? "blue" : args[1];
        } else if (args.length == 3 || args.length == 4) { // Use definition 2
            this.width = args[0];
            this.height = args[1];
            this.depth = args[2];
            this.color = args.length == 3? "blue" : args[3];
        } else {
            this.width = this.height = this.depth = 0;
            this.color = "blue";
        }
    }
}
function constructor_overload_ex1_test() {
    var dim: IDimension3D = { width: 10, height: 20, depth: 30};
    var box1: Box = new Box(dim);
    var box2: Box = new Box(dim, "red");
    var box3: Box = new Box(100, 200, 300);
    var box4: Box = new Box(100, 200, 300, "green");

    console.log(`box1.width = ${box1.width}, box1.color = ${box1.color}`); // 10 , blue
    console.log(`box2.width = ${box2.width}, box2.color = ${box2.color}`); // 10 , red
    console.log(`box3.width = ${box3.width}, box3.color = ${box3.color}`); // 100 , blue
    console.log(`box4.width = ${box4.width}, box4.color = ${box4.color}`); // 100 , green
}
constructor_overload_ex1_test(); // Call the function.
Output:
box1.width = 10, box1.color = blue
box2.width = 10, box2.color = red
box3.width = 100, box3.color = blue
box4.width = 100, box4.color = green
Constructor overloading example 2:
constructor_overload_ex2.ts
interface IPoint {
    x: number;
    y: number;
}
class Line {
    x1: number; y1: number; x2: number; y2: number;
    color: string;

    constructor(point: IPoint, color?: string); // definition 1
    constructor(point1: IPoint, point2: IPoint, color?: string); // definition 2
    constructor(...args: any[]) {
        if (args.length == 1) { // Use definition 1
            this.x1 = this.y1 = 0;
            this.x2 = args[0].x;
            this.y2 = args[0].y;
            this.color = "blue";
        } else if (args.length == 2) {
            if (typeof args[1] == "string") { // Use definition 1
                this.x1 = this.y1 = 0;
                this.x2 = args[0].x;
                this.y2 = args[0].y;
                this.color = args[1]
            } else { // Use definition 2
                this.x1 = args[0].x;
                this.y1 = args[0].y;
                this.x2 = args[1].x;
                this.y2 = args[1].y;
                this.color = "blue";
            }
        } else if (args.length >= 2) { // Use definition 3
            this.x1 = args[0].x;
            this.y1 = args[0].y;
            this.x2 = args[1].x;
            this.y2 = args[1].y;
            this.color = args[2];
        } else {
            this.x1 = this.y1 = this.x2 = this.y2 = 0;
            this.color = "blue";
        }
    }
}
function constructor_overload_ex2_test() {
    var point1: IPoint = { x: 10, y: 20 };
    var point2: IPoint = { x: 10, y: 20 };
    var line1: Line = new Line(point1, point2);
    var line2: Line = new Line(point2, "green");

    console.log(`line1.color = ${line1.color}`); // blue
    console.log(`line2.color = ${line2.color}`); // green
}
constructor_overload_ex2_test(); // Call the function.

7. Static factory method

Đôi khi việc sử dụng kỹ thuật "Constructor Overloading" như đã đề cập ở phần trên sẽ mang tới những phức tạp và khó hiểu trong quá trình sử dụng. Bạn nên cân nhắc sử dụng các phương thức nhà máy tĩnh (static factory method) như một giải pháp thay thế hiệu quả. Khác với constructor, bạn có thể tạo một hoặc nhiều phương thức nhà máy tĩnh.
static_factory_method_ex1.ts
interface IDimension3D {
    width: number;
    height: number;
    depth: number;
}
class Box3D {
    width: number;
    height: number;
    depth: number;
    color: string;

    constructor(width: number, height: number, depth: number, color: string) {
        this.width = width;
        this.height = height;
        this.depth = depth;
        this.color = color;
    }
    // Static factory method (To create Box3D object)
    static fromDimension3D(dimension: IDimension3D, color: string): Box3D {
        return new Box3D(dimension.width, dimension.height, dimension.depth, color);
    }
}
function static_factory_method_ex1_test() {
    var box1: Box3D = new Box3D(100, 200, 300, "red");

    var dim: IDimension3D = { width: 10, height: 20, depth: 30 };
    var box2: Box3D = Box3D.fromDimension3D(dim, "green"); // Call static method.

    console.log(`box1.width = ${box1.width}, box1.color = ${box1.color}`); // 100 , red
    console.log(`box2.width = ${box2.width}, box2.color = ${box2.color}`); // 10 , green
} 
static_factory_method_ex1_test(); // Call the function.
Output:
box1.width = 100, box1.color = red
box2.width = 10, box2.color = green