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});
หวังว่ามันจะช่วยได้บ้าง
