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

typescript - ใช้ตัวแปรหรือ generic เพื่อจำกัดชนิดของตัวแปรอีกตัวได้อย่างไร?


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

ตอนนี้ฉันมีอันนี้:

interface FooData {
  foo: string;
}

function renderFoo (data: FooData): string {
  return templateEngine.render("./foo.template", data)
}

interface SpamData {
  spam: number;
}

function renderSpam (data: SpamData): string {
  return templateEngine.render("./spam.template", data)
}

การเรียกไปที่ templateEngine.render จะรวมเส้นทางของเทมเพลตและข้อมูลเข้าด้วยกันอย่างง่าย ๆ โดยไม่ตรวจสอบประเภท ฉันกำลังมองหาการสร้างความปลอดภัยด้านประเภทเพิ่มเติมบนสิ่งนั้น

โค้ดด้านบนทำงานได้และทำให้แน่ใจว่า เช่น spam.template จะถูกเรนเดอร์เฉพาะกับข้อมูลประเภท SpamData แต่โครงสร้างนั้นยืดยาวและซ้ำซ้อน

ฉันคิดว่าอาจมีทางแก้ที่ให้ฟังก์ชันเดียวสำหรับเรียกใช้งาน (เช่น renderTemplate) ซึ่งมีลายเซ็นที่ (บางที?) บังคับรูปร่างของ data ตามเทมเพลตที่เลือก แต่ฉันยังใหม่เกินไปเกี่ยวกับประเภทข้อมูลที่จะเข้าใจว่าฉันกำลังถามอะไร หรือแม้กระทั่งวิธีทำ

คำถามของฉันคือ: สิ่งนี้สามารถปรับโครงสร้างใหม่ได้อย่างไร? ฉันก็เปิดรับความคิดเห็นโดยรวมด้วยหากดูเหมือนว่าฉันกำลังทำสิ่งผิดทิศทาง ความคิดเห็นของคุณเป็นที่ชื่นชม

คำตอบ #1

คุณควรจะแปลง FooData | SpamData เป็นยูเนียนที่มีการระบุชนิด (discriminated union) ด้วยคุณสมบัติ kind หรือ template, หรือ คุณควรส่งผ่านอาร์กิวเมนต์สองตัวไปยัง renderTemplate โดยตัวแรกเป็นอะไรบางอย่างเช่นสตริง kind หรือ template ในทั้งสองกรณีคุณควรเลือกตัวอักษรสตริงเพื่อแยกแยะชนิดข้อมูล ผมจะใช้ "foo" และ "spam" ที่นี่ ก่อนอื่น, ยูเนียนที่มีการระบุชนิด:

  interface FooData {
    kind: "foo";
    foo: string;
  }

  interface SpamData {
    kind: "spam";
    spam: number;
  }

  type Data = FooData | SpamData;

  function render(data: Data): string {
    return templateEngine.render("./" + data.kind + ".template", data);
  }

  render({ kind: "foo", foo: "hey" }); // okay
  render({ kind: "spam", spam: 123 }); // okay
  render({ kind: "foo", spam: 999 }); // error!

คุณสามารถเห็นได้ว่า Data เป็นการรวมกันของ FooData และ SpamData ซึ่งแต่ละตัวมีคุณสมบัติ kind ที่คุณสามารถใช้เพื่อแยกแยะว่ามันเป็นชนิดใด เป็นเรื่องโชคดีที่คุณสามารถสร้างเส้นทางแม่แบบด้วยการจัดการสตริงได้ แต่ถ้าวิธีนั้นใช้ไม่ได้สำหรับคุณ คุณสามารถตั้งค่าตารางค้นหาได้

วิธีการที่มีอาร์กิวเมนต์สองตัวจะมีลักษณะดังนี้:

  interface FooData {
    foo: string;
  }

  interface SpamData {
    spam: number;
  }

  interface DataMap {
    foo: FooData;
    spam: SpamData;
  }

  function render<K extends keyof DataMap>(kind: K, data: DataMap[K]): string {
    return templateEngine.render("./" + kind + ".template", data);
  }

  render("foo", { foo: "hey" }); // okay
  render("spam", { spam: 123 }); // okay
  render("foo", { spam: 999 }); // error!

ที่นี่เราได้คิดค้นอินเทอร์เฟซแผนที่ชื่อ DataMap ซึ่งแสดงถึงความสัมพันธ์ระหว่างสตริง kind และประเภทข้อมูล มันคล้ายกับ discriminated union แม้ว่าฉันจะใช้ฟังก์ชันทั่วไปเพื่อจับข้อจำกัดระหว่างอาร์กิวเมนต์สำหรับ render() จุดเดียวกันเกี่ยวกับการจัดการสตริงกับการค้นหาสำหรับการเรียก templateEngine.render() จริง ๆ ก็ใช้ได้ที่นี่เช่นกัน

หวังว่านั่นจะให้ไอเดียกับคุณได้ โชคดีนะ!

ลิงก์ไปยังโค้ด

คำตอบ #2

ความยากในกรณีของคุณคือเราต้องส่งพาธของเทมเพลตไปในฟังก์ชัน ซึ่งมันไม่สะดวกจริง ๆ (ดังที่ @lukasgeiter แสดง)

@jcalz เสนอวิธีแก้ปัญหาที่ดีสองข้อ แต่ต้องระวังบางจุด:

  • ยูเนียนที่ถูกจำแนกเป็นรูปแบบที่น่าสนใจ แต่ไม่สามารถใช้ได้เสมอไป ในกรณีนี้มันอยู่ในประเภทข้อมูล แต่ลองนึกภาพว่าข้อมูลนี้มาจากเซิร์ฟเวอร์ คุณสมบัติ kind ที่ใช้จำแนกอาจไม่สามารถมีอยู่ได้

  • การจัดการสตริงไม่ปลอดภัย ผมขอแนะนำให้ใช้แมป { [K in Kind]: TemplatePath; } การต่อสตริงตามนิยามแล้วถือว่าไม่ปลอดภัยตามประเภท คุณอาจพบข้อผิดพลาดเมื่อใช้เส้นทางที่ไม่ปกติ และการดีบักอาจใช้เวลานานขึ้น การใช้แมปจะทำให้คุณรวมแหล่งที่มาของบั๊กที่อาจเกิดขึ้นไว้ในค่าคงที่เพียงค่าเดียว ซึ่งสามารถดูแลรักษาได้ง่ายกว่า

ข้อเสนอแนะโค้ดของฉัน:

interface FooData {
    foo: string;
}

interface SpamData {
    spam: number;
}

interface TemplateMap {
    foo: FooData;
    spam: SpamData;
}

type Kind = keyof TemplateMap;

const templateKindMap: Readonly<{ [K in Kind]: string }> = {
    foo: './foo.template',
    spam: './spam.template'
};

function render<K extends Kind>(kind: K, data: TemplateMap[K]): string {
    return templateEngine.render(templateKindMap[kind], data);
}

render('foo', {foo: ''});
render('spam', {spam: 0});

หวังว่ามันจะช่วยได้บ้าง

บทความที่เกี่ยวข้อง