Interface ในภาษา TypeScript

13 February 2022

Interface, อินเตอร์เฟส คือวิธีการกำหนดประเภทข้อมูลให้กับออบเจ็คแบบนามธรรมเพื่อที่จะระบุ Property ที่ออบเจ็คสามารถมีและประเภทข้อมูลที่ Property จะมีได้ ในบทนี้ คุณจะได้เรียนรู้การใช้งาน Interface สำหรับกำหนดประเภทข้อมูลให้กับออบเจ็คในภาษา TypeScript นี่เป็นเนื้อหาในบทนี้

  • การใช้งาน Interface
  • Optional Properties
  • readonly Properties
  • Extending Types
  • Implementing Interface
  • Interface vs. Type

การใช้งาน Interface

โดยปกติแล้วการกำหนดประเภทข้อมูลให้กับออบเจ็คในภาษา TypeScript จะสามารถใช้ object เป็นประเภทข้อมูลในตอนประกาศตัวแปรได้ แต่ประเภทข้อมูลนี้จะทำให้ออบเจ็คสามารถมีรูปร่างเป็นแบบไหนก็ได้ หรือกล่าวอีกนัยหนึ่ง มันสามารถมีจำนวนของ Property และประเภทข้อมูลเป็นแบบอะไรก็ได้

ในภาษา TypeScript นั้นเราสามารถใช้คำสั่ง interface สำหรับสร้างประเภทข้อมูลใหม่เพื่อกำหนดโครงสร้างของออบเจ็คให้มีความเจาะจงมากขึ้น ต่อไปเรามาสร้าง Interface สำหรับออบเจ็คที่ใช้เก็บข้อมูลของผู้ใช้งาน นี่เป็นตัวอย่าง

interface User {
    id: number;
    name: string;
};

ในตัวอย่างนี้ เป็นการสร้าง Interface ชื่อว่า User ที่มีสอง Property คือ id มีประเภทข้อมูลเป็นตัวเลขและ name มีประเภทข้อมูลเป็น String เมื่อ Interface ถูกสร้างแล้ว เราสามารถนำมันไปใช้เป็นประเภทข้อมูลของออบเจ็คได้ และนี่เป็นตัวอย่างการใช้งาน

user_type.ts
interface User {
    id: number;
    name: string;
};

let user1: User = {
    id: 1,
    name: "Alex"
};

let user2: User = {
    id: 2,
    name: "Julia"
};

ในตัวอย่าง หลังจากที่ประกาศ Interface แล้ว เราได้ประกาศสองออบเจ็คโดยกำหนดประเภทข้อมูลเป็น User สังเกตว่าทั้งสองออบเจ็คจะมี Property และประเภทข้อมูลที่ตรงกับที่ระบุใน Interface และนี่เป็นวิธีที่เราควบคุมโครงสร้างของออบเจ็คโดยใช้ Interface ในภาษา TypeScript

Interface เป็นประเภทข้อมูลที่สามารถใช้ซ้ำๆ ได้เหมือนกับประเภทข้อมูลพื้นฐานอื่นๆ ในภาษา TypeScript เช่น number string หรือ boolean นั่นหมายความว่ามันสามารถนำไปใช้ได้ในทุกทีี่ของโปรแกรม นี่เป็นตัวอย่างของโปรแกรมหาระยะห่างระหว่างจุดสองจุดในสองมิติ

point_distance.ts
interface Point {
    x: number;
    y: number;
};

function distance(p1: Point, p2: Point): number {
    return Math.sqrt(
        Math.pow(p2.x - p1.x, 2) +
        Math.pow(p2.y - p1.y, 2)
    );
}

let p1: Point = { x: 1, y: 3 };
let p2: Point = { x: -3, y: 5 };

console.log(`p1: (${p1.x}, ${p1.y})`);
console.log(`p2: (${p2.x}, ${p2.y})`);
console.log(`Distance: ${distance(p1, p2)}`);

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

p1: (1, 3)
p2: (-3, 5)
Distance: 4.47213595499958

ในตัวอย่างนี้ เราได้สร้างประเภทข้อมูล Point ที่มีสอง Property x และ y สำหรับเก็บพิกัดของจุดในสองมิติ จากนั้นเราใช้มันเป็นประเภทข้อมูลให้กับตัวแปรในตอนประกาศออบเจ็ค p1 และ p2 และใช้มันเป็นประเภทข้อมูลให้กับพารามิเตอร์ของฟังก์ชัน distance ซึ่งนี่เป็นเรื่องปกติของการใช้งานประเภทข้อมูลในภาษา TypeScript

นอกจากนี้ สำหรับแต่ละ Property ที่กำหนดใน Interface นั้นเราสามารถกำหนดคุณสมบัติเพิ่มเติมได้ว่าจะให้แต่ละ Property เป็นแบบทางเลือก (Optional) หรืออ่านได้อย่างเดียว (Read-only) หรือไม่ มาดูตัวอย่างการใช้งานคุณสมบัติเหล่านี้กันต่อ

Optional Properties

การกำหนด Property ให้เป็นแบบทางเลือกสามารถทำได้โดยใส่เครื่องหมาย ? หลังชื่อของ Property นี่จะทำให้ออบเจ็คสามารถที่จะมี Property ในตอนที่กำหนดค่าของออบเจ็ค Literal หรือไม่ก็ได้ ถ้าหากเราไม่ได้กำหนด undefined จะเป็นค่าเริ่มต้นสำหรับ Property นี่เป็นตัวอย่างการทำให้ Property เป็นแบบทางเลือกที่ใช้ออบเจ็คเก็บข้อมูลเกี่ยวกับเพลง

optional_properties.ts
interface Song {
    name: string;
    artist: string;
    year?: number;
};

let song1: Song = {
    name: "Sometimes",
    artist: "Chelsea Culter",
    year: 2019
};

let song2: Song = {
    name: "Scared",
    artist: "Jeremy Zucker"
};

function showInfo(song: Song): void {
    let text: string = `${song.name} by ${song.artist}`;
    if (typeof song.year !== "undefined") {
        text += ` (${song.year})`;
    }
    console.log(text);
}

showInfo(song1);
showInfo(song2);
showInfo({
    name: "If You See Her",
    artist: "LANY",
    year: 2561
});

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

Sometimes by Chelsea Culter (2019)
Scared by Jeremy Zucker
If You See Her by LANY (2561)

ในตัวอย่างนี้ แสดงให้คุณเห็นถึงการกำหนด Property ให้เป็นแบบทางเลือก เราได้สร้าง Interface ชื่อว่า Song สำหรับเก็บข้อมูลของเพลงที่ประกอบไปด้วยสาม Property โดยภายในนั้นมี year ที่เป็น Property แบบทางเลือกที่ถูกกำหนดเครื่องหมาย ? ไว้ที่หลังชื่อของมัน

จากนั้นเราสร้างสองออบเจ็ค song1 และ song2 เพื่อเก็บข้อมูลของเพลง จะเห็นว่าเนื่องจาก Property year นั้นเป็นแบบทางเลือก มันจึงสามารถมีหรือไม่มีก็ได้ สำหรับออบเจ็คที่สองเราไม่ได้กำหนดค่าปีให้กับออบเจ็ค มันอาจจะเป็นเพราะว่ายังไม่ข้อมูลเกี่ยวกับปีที่เผยแพร่ของเพลงในขณะนี้หรืออาจเป็นเพราะความตั้งใจของเราเองก็ได้

readonly Properties

Property ของออบเจ็คสามารถที่จะกำหนดคุณสมบัติเพื่อให้สามารถอ่านค่าได้อย่างเดียวได้ เราสามารถทำเช่นนี้ได้ด้วยการใส่คำสั่ง readonly ที่หน้าชื่อของ Property นี่จะทำให้การดำเนินการที่ส่งผลต่อการเปลี่ยนแปลงค่าของ Property ไม่สามารถทำได้ ในตัวอย่างนี้ มาทำให้บาง Property บนออบเจ็คสามารถอ่านได้อย่างเดียวกัน

read_properties.ts
interface Circle {
    radius: number,
    readonly color: string
};

let circle: Circle = {
    radius: 5,
    color: "Red"
};

console.log(`A ${circle.color} circle has radius of ${circle.radius}.`);

circle.radius = 6;
circle.color = "Green";

console.log(`A ${circle.color} circle has radius of ${circle.radius}.`);

นี่เป็นข้อผิดพลาดที่เกิดขึ้นเมื่อเรารันโปรแกรม

Cannot assign to 'color' because it is a read-only property.

ในตัวอย่างนี้ โปรแกรมแสดงข้อผิดพลาดว่าไม่สามารถเปลี่ยนค่าของ color ได้ เนื่องจากมันเป็น Property ที่สามารถอ่านค่าได้เพียงอย่างเดียว นั่นหมายความว่าเราสามารถดำเนินการใดๆ ที่ส่งผลต่อการเปลี่ยนแปลงค่าของ Property ดังกล่าวได้ อย่างไรก็ตาม เพื่อทำให้โปรแกรมนี้ทำงานได้ เรามีสองทางเลือกคือลบบรรทัดของโค้ดที่ทำให้เกิดข้อผิดพลาดออก

// Remove this line to prevent error
circle.color = "Green";

หรือทางเลือกที่สองคือการลบคำสั่ง readonly หน้า Property color เพื่อทำให้มันสามารถกำหนดค่าได้อีกครั้ง และนี่เป็นผลลัพธ์จากการเลือกใช้วิธีแรกที่นำคำสั่งที่เปลี่ยนค่า color ออกและรันโปรแกรมอีกครั้ง

A Red circle has radius of 5.
A Red circle has radius of 6.

นี่มีเหตุผลเนื่องจากเมื่อเรากำหนด Property ให้เป็นแบบอ่านได้อย่างเดียว หมายความว่าเราไม่ต้องการเปลี่ยนแปลงค่าของมัน และแม้ว่าเราสามารถป้องกันด้วยการไม่เปลี่ยนค่ามันด้วยตัวเอง แต่การใช้คำสั่ง readonly จะทำให้มันได้รับการตรวจสอบโดย TypeScript และช่วยป้องกันการเปลี่ยนแปลงแบบไม่ตั้งใจได้

Extending Types

ในการกำหนดประเภทข้อมูลด้วย Interface นั้นเราสามารถสร้าง Interface ใหม่โดยมีพื้นฐานมาจาก Interface เดิมได้ด้วยคำสั่ง extends ซึ่งนี่เรียกกว่าการสืบทอด Interface และมันมีประโยชน์เมื่อประเภทข้อมูลใหม่ที่จะสร้างต้องการมีทุกอย่างเหมือนกับประเภทข้อมูลที่สร้างไว้แล้ว แต่เพียงต้องการเพิ่มบางอย่างเข้าไป

นี่เป็นตัวอย่างการสร้างประเภทข้อมูลเพื่อเก็บตำแหน่งในสองมิติและสามมิติ โดยเราจะใช้ความสามารถการสืบทอดของ Interface ในภาษา TypeScript

extending_interface.ts
interface Point2D {
    x: number;
    y: number;
};

interface Point3D extends Point2D {
    z: number
};

let p1: Point3D = {
    x: -1,
    y: 0,
    z: 3
};

let p2: Point2D = {
    x: 2,
    y: 6
};

console.log("3D point", p1);
console.log("2D point", p2);

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

3D point { x: -1, y: 0, z: 3 }
2D point { x: 2, y: 6 }

ในตัวอย่างนี้ เริ่มต้นเราได้สร้างประเภทข้อมูล Point2D ที่เป็นออบเจ็คของตำแหน่งในสองมิติที่ประกอบไปด้วยสอง Property ได้แก่ x และ y ที่มีประเภทข้อมูลเป็นตัวเลขสำหรับเก็บพิกัดในสองมิติ จากนั้นเราสร้างประเภทข้อมูล Point3D ที่เป็นออบเจ็คของตำแหน่งในสามมิติที่สืบทอดมาจาก Point2D

interface Point3D extends Point2D {
    z: number
};

อย่างที่เราทราบว่าตำแหน่งในระนาบสามมิตินั้นจะมีแกน z เพิ่มเข้ามา และเนื่องจากสำหรับแกน x และ y ถูกสร้างไปก่อนหน้าแล้วใน Point2D นั่นทำให้เราสืบทอดมันมาใช้ใน Point3D ได้โดยใช้คำสั่ง extends ตามด้วยชื่อของ Interface ที่ต้องการสืบทอด นี่จะทำให้เราไม่ต้องเขียนทุกอย่างใหม่ทั้งหมด

let p1: Point3D = {
    x: -1,
    y: 0,
    z: 3
};

let p2: Point2D = {
    x: 2,
    y: 6
};

และแม้ว่าประเภทข้อมูล Point2D จะถูกนำไปสืบทอดโดย Point3D แต่เราก็ยังสามารถนำมันมาใช้งานเพื่อสร้างออบเจ็คได้ปกติ แนวคิดการสืบทอด Interface ในภาษา TypeScript นี้ได้รับแรงบรรดาลใจมาจากการเขียนโปรแกรมเชิงวัตถุ (OOP) ที่เป็นคุณสมบัติการสืบทอดของคลาส

Implementing Interface

Interface สามารถที่จะนำไป Implement โดยคลาสได้ แนวคิดนี้ได้รับมาจากการเขียนโปรแกรมเชิงวัตถุที่หลายคลาสสามารถ Implement มาจาก Interface เดียวกันได้ ยกตัวอย่างเช่น เราสามารถออกแบบหลายคลาสที่ต้องการมีแอททริบิวต์และเมธอดที่เหมือนกัน แต่แตกต่างกันที่วิธีการทำงาน เป็นต้น

นี่เป็นตัวอย่างการนำ Interface ไป Implement ในภาษา TypeScript โดยเราจะสร้างคลาสของสัตว์ที่จะมีการ Implement มาจาก Interface เดียวกัน โดยคลาสเหล่านี้จะมีแอททริบิวต์และเมธอดที่เหมือนกัน แต่แตกต่างกันที่วิธีการทำงาน

implementing_interface.ts
interface Animal {
    name: string;
    move: () => void;
    speak: () => void;
};

class Dog implements Animal {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    move(): void {
        console.log(`[Dog] ${this.name} is running...`);
    }

    speak(): void {
        console.log(`[Dog] ${this.name}: Bow bow!`);
    }
}

class Cat implements Animal {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    move(): void {
        console.log(`[Cat] ${this.name} is running...`);
    }

    speak(): void {
       console.log(`[Cat] ${this.name}: Meow meow!`); 
    }
}

let d: Dog = new Dog("James");
let c: Cat = new Cat("Billy");

d.move();
d.speak();

c.move();
c.speak();

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

[Dog] James is running...
[Dog] James: Bow bow!
[Cat] Billy is running...
[Cat] Billy: Meow meow!

ในตัวอย่างนี้ เราได้สร้าง Interface Animal ที่ประกอบไปด้วยหนึ่ง Property และสองเมธอด จากนั้นเราสร้างสองคลาส Dog และ Cat ที่ทำการ Implement มาจาก Interface นี้ ซึ่งนี่จะสามารถทำได้โดยใช้คำสั่ง implements ตามด้วยชื่อของ Interface ที่ต้องการ

class Dog implements Animal {
    ...
}

class Cat implements Animal {
    ...
}

เนื่องจากทั้งสองคลาสได้ทำการ Implement มาจาก Interface Animal มันจะต้องมีการกำหนด Property และเมธอดที่เหมือนกับ Interface นี้มี แต่การทำงานของเมธอดในแต่ละคลาสนั้นสามารถที่จะแตกต่างกันได้ นี่ก็สอดคล้องกับความจริงที่ว่าสัตว์แต่ละชนิดสามารถที่จะพูดได้ แต่วิธีการพูดของมันอาจแตกต่างกัน

นอกจาก Interface จะสามารถถูกนำไป Implement โดยคลาสแล้ว มันยังสามารถนำไปใช้เป็นประเภทข้อมูลของคลาสในตอนที่ประกาศตัวแปรหรือในพารามิเตอร์ของฟังก์ชันได้ นี่เป็นตัวอย่าง

function doThings(a: Animal): void {
    a.move();
    a.speak();  
}

let d: Animal = new Dog("James");
let c: Animal = new Cat("Billy");

doThings(d);
doThings(c);

ในตัวอย่างนี้ จะเห็นว่า Interface สามารถใช้เป็นประเภทข้อมูลที่สืบทอดไปจากมันได้ นั่นทำให้เราสามารถใช้ Animal เป็นประเภทข้อมูลให้กับออบเจ็คที่สร้างมาจากคลาส Dog และ Cat เมื่อเราใช้ Interface ในการกำหนดประเภทข้อมูลให้กับตัวแปรหรือพารามิเตอร์ของฟังก์ชัน มันเป็นการบังคับว่าออบเจ็คจะต้องมีโครงสร้างเหมือนกับ Interface โดยการเพิกเฉยคลาสที่ออบเจ็คสร้างมาจาก

และในกรณีนี้พารามิเตอร์ a ในเมธอด doThings จะรับทุกออบเจ็คที่มีโครงสร้างเหมือนกับ Interface Animal และเราสามารถแน่ใจได้ว่ามันจะมีเมธอด move และ speak เหมือนกับที่ระบุไว้ใน Interface แต่ไม่ได้สนใจว่าเมธอดจะมีการทำงานอย่างไร ซึ่งนี่เป็นแนวคิดของ Polymorphism ในการเขียนโปรแกรมเชิงวัตถุ

Interface vs. Type

คุณอาจจะทราบว่าเราสามารถใช้คำสั่ง type เพื่อกำหนดประเภทข้อมูลให้กับออบเจ็คได้เหมือนกับ Interface แต่ในการสร้างประเภทข้อมูลให้กับออบเจ็คในภาษา TypeScript นั้น เราแนะนำให้ใช้ Interface เป็นอันดับแรกและใช้คำสั่ง type เป็นอันดับที่สองหรือในกรณีที่ Interface ไม่มีคุณสมบัติที่ต้องการเท่านั้น

ทั้งคำสั่ง type และ Interface นั้นสามารถกำหนดประเภทข้อมูลให้กับออบเจ็คได้ นี่ทำให้เราสามารถควบคุม Property และประเภทข้อมูลที่ Property เหล่านั้นจะมี การทำงานของมันไม่แตกต่างคุณสามารถใช้แบบไหนก็ได้ นี่เป็นตัวอย่าง

type_n_interface.ts
type Type1 = {
    id: number,
    name: string
};

interface Type2 {
    id: number;
    name: string;
};

let user1: Type1 = {
    id: 1,
    name: "John"
};

let user2: Type2 = {
    id: 2,
    name: "Julia"
};

ในตัวอย่างนี้ จะเห็นว่าในการกำหนดประเภทข้อมูลให้กับออบเจ็ค ทั้งคำสั่ง type และ Interface ให้ผลลัพธ์การทำงานที่เหมือนกัน เราได้สร้างประเภทข้อมูลสำหรับออบเจ็คที่ประกอบไปด้วยสอง Property โดยมี id ที่เป็นตัวเลขและมี name ที่เป็น String

อย่างไรก็ตาม ทุกคำสั่งได้ถูกออกแบบมาเพื่อวัตถุประสงค์ที่แตกต่างกัน และสิ่งที่แตกต่างระหว่างทั้งสองคำสั่งที่คำสั่ง type สามารถทำได้ แต่ Interface ทำไม่ได้ก็คือการใช้กำหนดประเภทข้อมูลในรูปแบบอื่นที่ไม่ใช่ออบเจ็ค เช่น ประเภทข้อมูลพื้นฐาน Union type หรือ Tuple เป็นต้น ยกตัวอย่างเช่น

// primitives
type NameType = string;

// union types
type NumberOrString = number | string;

// tuples
type NumberStringPair = [number, string];

จากในตัวอย่างนี้ การประกาศประเภทข้อมูลทั้งสามรูปแบบนี้จะสามารถทำได้ด้วยคำสั่ง type เท่านั้น และจะทำไม่ได้โดยคำสั่ง Interface

สำหรับสิ่งที่คำสั่ง Interface สามารถทำได้แต่คำสั่ง type ทำไม่ได้ก็จะมีการ extends และการ implements ที่คุณได้เรียนรู้ไปแล้วในส่วนก่อนหน้า และนอกจากนี้ Interface ยังสามารถแยกการประกาศออกเป็นส่วนๆ ได้ นี่เรียกว่า Declaration merging ยกตัวอย่างเช่น

declaration_merging.ts
interface Point {
    x: number;
};

interface Point {
    y: number;
};

let point: Point = {
    x: 1,
    y: 2
};

นี่เป็นวิธีการประกาศ Interface แบบแยกส่วนที่จะถูกใช้ในกรณีที่เราต้องการแยกการประกาศ Interface ในหลายสถานที่ภายในโปรแกรม และเนื่องจากเมธอดในภาษา Typescript สามารถทำการ Overload ได้ ดังนั้นการประกาศ Interface ในรูปแบบนี้อาจมีประโยชน์ในโปรแกรมที่ใหญ่และมีความซับซ้อน นี่เป็นอีกตัวอย่างของการประกาศ Interface แบบแยกส่วน

declaration_merging2.ts
interface Document {
    createElement(tagName: any): Element;
}

interface Document {
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
}

interface Document {
    createElement(tagName: "img"): HTMLImageElement;
    createElement(tagName: "video"): HTMLVideoElement;
}

ในตัวอย่างนี้ เป็นการสร้าง Interface ที่มี Overload เมธอด createElement ที่ใช้สำหรับสร้างอีลีเมนต์ของ HTML ประเภทต่างๆ โดยแยกการประกาศออกจากกันตามประเภทของอีลีเมนต์ ในตอนท้ายเราจะได้ Interface เดียวที่มีชื่อว่า Document ที่ประกอบไปด้วยเวอร์ชันทั้งหมดของเมธอด

อย่างไรก็ตามในแง่ของการทำงาน การประกาศแบบแยกไม่ได้มีข้อแตกต่างอะไรกับการประกาศแบบรวมกัน แต่มันเพียงช่วยทำให้โค้ดแยกออกจากกัน และอาจจะจัดการได้ง่ายขึ้น นี่ขึ้นกับความพึงพอใจของโปรแกรมเมอร์ว่าจะเลือกใช้วิธีไหนในการทำงาน

ในบทนี้ คุณได้เรียนรู้การใช้งาน Interface ในภาษา TypeScript เพื่อกำหนดประเภทข้อมูลให้กับออบเจ็ค เราได้พูดถึงการใช้งานคุณสมบัติต่างๆ เช่น การกำหนดคุณสมบัติให้กับ Property การสืบทอด การ Implement เป็นต้น นอกจากนี้ คุณได้ทราบความแตกต่างระหว่างการใช้งาน Interface กับคำสั่ง type

บทความนี้เป็นประโยชน์หรือไม่?Yes·No