ค้นหาเว็บไซต์

typescript - มีประเภทของคลาสที่ยังไม่ได้สร้างอินสแตนซ์หรือไม่?


สมมติว่าโค้ดต่อไปนี้:

interface Foobar {}

class Foo implements Foobar {}
class Bar implements Foobar {}

const arr: Foobar[] = [Foo, Bar];

สิ่งนี้สามารถคอมไพล์ได้ แต่ มันไม่ใช่ความจริงทั้งหมด typeof Foo หรือ typeof Bar เป็น Function - มีเหตุผล เพราะพวกมันยังไม่ได้ถูกสร้างอินสแตนซ์จนถึงตอนนี้

มีวิธีที่ดีกว่าในการจัดเก็บคลาสที่ยังไม่ได้สร้างอินสแตนซ์มากกว่า arr: any[] ไหม?

คำตอบ #1

อินเทอร์เฟซว่างที่นี่ทำให้คุณสับสน; โค้ดนี้ใช้งานได้เพียงเพราะอินเทอร์เฟซไม่มีสมาชิกใด ๆ (และด้วยเหตุนี้จึงหมายถึง "อะไรก็ตาม"):

interface Foobar { x: number; }

class Foo implements Foobar { x: number; }
class Bar implements Foobar { x: number; }

// Error
const arr: Foobar[] = [Foo, Bar];

ประเภททั่วไปของคลาสคือ { new(...args: any[]): any } หากคุณกำลังพูดถึงบางสิ่งที่สามารถถูกเรียกใช้ด้วย new หรือ { new(...args: any[]): Foobar } หากคุณกำลังพูดถึงบางสิ่งที่ส่งกลับ Foobar เมื่อถูกเรียกใช้ด้วย new.

คำตอบ #2

interface Foobar {}

class Foo implements Foobar {}
class Bar implements Foobar {}

const arr: Foobar[] = [Foo, Bar];

นี่คอมไพล์ได้ แต่จริงๆ แล้วมันไม่ใช่ความจริง typeof Foo หรือ typeof Bar คือ Function - เหมาะสมแล้ว เพราะว่าพวกมันยังไม่ได้ถูกสร้างอินสแตนซ์เลยจนถึงตอนนี้

คลาสคือค่าหนึ่ง อินสแตนซ์คือค่าหนึ่ง ไม่มีสิ่งที่เรียกว่า คลาสที่ไม่ได้ถูกสร้างอินสแตนซ์

มันมีประโยชน์อย่างมากที่จะสามารถเก็บคลาสได้ ไม่ใช่แค่เฉพาะอินสแตนซ์ของมัน

ยิ่งไปกว่านั้น เนื่องจากคลาสเป็นเพียงฟังก์ชัน ความสามารถในการทำเช่นนั้นจึงเป็นผลลัพธ์ตามธรรมชาติ เป็นคุณสมบัติที่ทรงพลังซึ่งเราได้รับโดยไม่ต้องเสียค่าใช้จ่าย

มีวิธีที่ดีกว่าในการเก็บคลาสที่ยังไม่ได้สร้างวัตถุมากกว่า arr: any[] ไหม?

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

วิธีที่ง่ายที่สุดคือปล่อยให้คอมไพเลอร์เดาประเภทเอง วิธีนี้มีความแม่นยำสูงและเขียนง่าย (เพียงเขียนโค้ดเพื่อลบ ไม่ใช่เพิ่ม)

const arr = [Foo, Bar];

อีกวิธีหนึ่งคือการใช้ประเภททูเพิล

const arr: [typeof Foo, typeof Bar] = [Foo, Bar];

และสุดท้ายคุณสามารถใช้ชนิดของยูเนียนแบบชัดเจน

const arr: Array<typeof Foo | typeof Bar> = [Foo, Bar];

สำหรับการใช้คลาสโดยไม่ตั้งใจ ลืม new มัน คำตอบของ Ryan Cavanaugh ครอบคลุมเรื่องนั้นได้อย่างสมบูรณ์

ตอบกลับความคิดเห็น

นี่น่าจะทำให้การใช้งานอินเทอร์เฟซค่อนข้างไร้ประโยชน์ใช่ไหม?

อินเทอร์เฟซ (interface) คือการประกาศที่ใช้ในการอธิบายชนิดของค่าหรือข้อมูลใด ๆ

TypeScript เป็นภาษาแบบชนิดเชิงโครงสร้าง (หรือที่เรียกว่า duck typed) ซึ่งหมายความว่าค่าทุกค่าจะใช้อินเทอร์เฟซได้เพียงโดยมีสมาชิกที่เข้ากันได้

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

คุณอาจกำลังคิดถึงการสั่งงานแบบ interface ดังนั้นประเภท FooBar[] ในตัวอย่างของคุณ แต่ใน TypeScript เช่นเดียวกับใน JavaScript การมีพหุรูปร่าง (polymorphism) จะบรรลุผลผ่านเทคนิคที่แตกต่างกันโดยสิ้นเชิง

นี่คือตัวอย่างของการใช้ interface เพื่ออธิบายอินสแตนซ์ของคลาส:

interface I {
  p: string;
  m(): void;
}

class A implements I {
  p = 'implements I';
  m() {console.log(this.p);}
}

const a = new A;

class B {
  p = 'also implements I';
  m() {console.log(this.p);}
}

const b = new B;

const c = {
  p: 'also implements I',
  m() {console.log(this.p);}
};

const is: I[] = [a, b, c];

อินเทอร์เฟซ I ข้างต้นอธิบายรูปร่างของอินสแตนซ์ของ คลาส A รูปร่างของอินสแตนซ์ของอินสแตนซ์ของ คลาส B และรูปร่างของออบเจ็กต์ c

ด้วยเหตุนี้ การที่คลาสเองสามารถนำอินเทอร์เฟซไปใช้งานได้เองก็ถือเป็นเรื่องธรรมชาติ

อินเทอร์เฟซดังกล่าวอธิบายรูปทรงของค่าคลาสเอง ไม่ใช่ของอินสแตนซ์ของมัน

จงจำไว้ว่า คลาสใน JavaScript เป็นเพียงฟังก์ชันที่สามารถถูก "สร้างใหม่" (ทางเทคนิคมากขึ้นคือ [[Construct]]ed)

ด้วยเหตุนี้ อินเทอร์เฟซที่แทนคลาสใด ๆ สามารถเขียนได้

interface Newable<T> {
  new(...args: any[]): T;
}

นี่บางครั้งถูกเรียกว่าประเภทของด้านคงที่ของคลาสเนื่องจากอนุญาตให้ระบุสมาชิก static ในอินเทอร์เฟซได้

interface Deserializable<T> {
  fromJSON(json: string): T;
}

interface NewableDeserializable<T> extends Newable<T>, Deserializable<T> {}

ตอนนี้พิจารณาอินเทอร์เฟซและคลาสต่อไปนี้

interface Character {
  position: {x: number, y: number, z: number};
  name: string;
}

class Snake implements Character {
  position = {x: 0, y: 0, z: 0};
  name = 'Snake';

  static fromJSON(saved: string) {
    const mc = new this();

    return Object.assign(mc, JSON.parse(saved));
  }
}

interface NewableDeserializable<T> extends Newable<T>, Deserializable<T> {}

const deserializableMainCharacterClass: NewableDeserializable<Character> = Snake;

การระบุชนิดข้อมูลบนตัวแปรเป็นทางเลือก แต่แสดงให้เห็นว่า คลาสเอง ใช้อินเทอร์เฟซนั้นเพราะว่ามันตรวจสอบชนิดได้

สุดท้าย สิ่งเหล่านี้ทั้งหมดสามารถทำได้ในหนึ่งนิพจน์:

const Meryl: NewableDeserializable<Character> =
  class implements Character {
    name = 'Meryl';
    position = {x: 1, y: 1, z: 1};

    static fromJSON(saved: string) {
      const mc = new this();
      return Object.assign(mc, JSON.parse(saved));
    }
};