typescript - วิธีการเก็บการพิมพ์ชื่อคีย์แบบอัตโนมัติเป็นส่วนหนึ่งของอินเทอร์เฟซในขณะที่ยังระบุประเภทค่าที่ TS?
ฉันอยากจะสามารถกำหนดอินเทอร์เฟซ/ประเภทของวัตถุที่อนุญาตให้ฉันกำหนดค่าของมันได้ แต่ใช้คีย์ของวัตถุเป็นชุดของคีย์ที่เป็นไปได้ในประเภทนั้น เมื่อมองภาพ ฉันต้องการหาทางเลือกแทน:
interface IElementType = {[key: string]: SpecialProperties}และแทนที่ด้วยบางอย่างเช่น
interface IElementType = {[key: keyof elements]: SpecialProperties}ในตัวอย่างเช่น
const elements: IElementType = {
pirate: {
name: 'patchy',
type: 'matey'
},
captain: {
name: 'peggy',
type: 'pegleg'
},
...
}
// for reference
interface SpecialProperties {
name: string,
type: string
}
โดยที่ keyof elements คือฉันกำลังคิดขึ้นมาเองในสิ่งที่ฉันอยากให้เป็นไปได้ (ทราบว่าสิ่งนี้ใช้ไม่ได้เพราะ elements เป็นวัตถุ js) แต่หวังว่าฉันอาจคิดผิดไป แรงกระตุ้นที่นี่คือการรักษาข้อมูลประเภท/intellisense เมื่อทำการนำเข้าตัวแปรนี้ไปยังส่วนอื่นของโปรเจกต์ ฉันสามารถพิมพ์ elements.pirate และ ts จะรู้ว่าคีย์นี้มีอยู่จริง เมื่อเราใช้ [key: string] เราโดยนัยกำลังทำให้คีย์ในวัตถุเป็นอนามัยและทิ้งข้อมูลนั้นไป แตกต่างจากเมื่อเรากำหนดตัวแปรโดยไม่มีการระบุประเภทอย่างชัดเจน เช่น
const elements = { ... }ที่ TypeScript กำหนดประเภทของตัวแปรนี้โดยปริยายด้วยประเภทที่มีคีย์ชัดเจนที่วัตถุมี ดังนั้นในกรณีข้างต้น ประเภทจะตรงกับค่าของวัตถุทั้งหมดพอดี
ฉันหวังว่าจะมีจุดกึ่งกลางตรงนี้ที่เราสามารถรักษาการพิมพ์แบบแฝงของชื่อคีย์ไว้ได้ แต่บังคับให้ค่าของมันเป็นค่าเฉพาะได้ สิ่งนี้เป็นไปได้ไหม?
คำตอบ #1
ก็ คุณสามารถ แสดงออก สิ่งที่คุณกำลังพูดได้ เนื่องจาก keyof typeof elements เป็นชนิดที่ถูกต้องสมบูรณ์ แต่คุณจะไม่สามารถนิยาม IElementType ในแง่ของ elements และในทางกลับกันโดยไม่เกิดคำเตือนเรื่องความเป็นวงกลม:
type IElementType = Record<keyof typeof elements, SpecialProperties>;
// ~~~~~~~~~~~~ <-- Type alias 'IElementType' circularly references itself.
const elements: IElementType = { /* .. */ }
// ~~~~~~~~ <-- 'elements' is referenced directly or indirectly in its own type annotation.
ดังนั้นเราจำเป็นต้องละทิ้งความคิดนั้น
ถ้าคุณใส่คำอธิบายประกอบให้กับ elements ด้วยประเภทที่ยังไม่รู้จักคีย์เฉพาะ เช่น {[k: string]: SpecialProperties} ที่มี index signature ตัวคอมไพเลอร์จะ ขยาย elements ไปเป็นประเภทนั้นและลืมคีย์เฉพาะเหล่านั้น
เนื่องจากคุณต้องการที่จะสามารถกำหนด elements ในลักษณะที่ตัวคอมไพเลอร์ ตรวจสอบ โดยไม่ขยายชนิดออก ดังนั้น คุณไม่สามารถทำการกำกับประเภทมันได้ มีคำร้องขอฟีเจอร์ที่ microsoft/TypeScript#7481 สำหรับการเพิ่มโอเปอเรเตอร์ชนิดบางอย่างที่ทำหน้าที่เหมือนกับการกำกับประเภทแบบไม่ขยาย แต่ในตอนนี้มันยังไม่ใช่ส่วนหนึ่งของภาษา
แทนที่จะทำเช่นนั้น คุณสามารถกำหนดฟังก์ชันช่วยเหลือแบบทั่วไปที่คุณใช้แทนการใช้คำอธิบายประเภท คุณจะเปลี่ยน const elements: IElementType=someValue เป็น const elements=asElementType(someValue) โดยที่ asElementType() ถูกกำหนดดังนี้:
const asElementType = <K extends PropertyKey>(t: { [P in K]: SpecialProperties }) => t;
ฟังก์ชันนี้จะยอมรับอินพุตก็ต่อเมื่อมันสามารถสรุปประเภทคีย์ K ได้ (ซึ่งอาจเป็นยูเนียนของคีย์) โดยที่อินพุตมีคีย์ K และค่าของ SpecialProperties เนื่องจากฟังก์ชันเพียงแค่ส่งกลับอินพุตของมัน ชนิดของค่าที่ส่งกลับจะเหมือนกับอินพุต: เป็นประเภทอ็อบเจ็กต์ที่มีบางส่วนของประเภทคีย์ที่สรุปได้ K และประเภทค่าของ SpecialProperties
มาลองดูมันในการใช้งานกันเถอะ:
const elements = asElementType({
pirate: {
name: 'patchy',
type: 'matey'
},
captain: {
name: 'peggy',
type: 'pegleg'
},
});
/* const elements: {
pirate: SpecialProperties;
captain: SpecialProperties;
} */
นั่นใช้ได้; ตอนนี้ elements มีชนิดเป็น {pirate: SpecialProperties, captain: SpecialProperties} และถ้าคุณทำผิดพลาด คอมไพเลอร์จะแจ้งเตือนคุณ:
const oops = asElementType({
swabbie: { // error! Property 'type' is missing
name: 'bilgey'
}
})
จึงแสดงให้เห็นว่า asElementType() ทำงานคล้ายกับอินเทอร์เฟซที่มีประเภทคีย์แบบไดนามิก แต่มีประเภทค่าคงที่
ลิงก์ Playground ไปยังโค้ด
คำตอบ #2
สำหรับตัวอย่างนี้...
const elements: IElementType = {
pirate: {
name: 'patchy',
type: 'matey'
},
captain: {
name: 'peggy',
type: 'pegleg'
},
...
}
...ในการทำงาน ให้กำหนด IElementType เป็นอินเทอร์เฟซแบบทั่วไป แต่การใช้การประกาศ interface ไม่สามารถใช้ได้ในกรณีนี้. :(
type IElementType<K extends string> = {
[x in K]: SpecialProperties;
};
ตัวแปร elements จะมีชนิดเป็น IElementType<'pirate' | 'captain'> สำหรับสองคีย์แรก
