การกำหนดฟังก์ชันเข้าถึงวัตถุเจเนอริกที่ปลอดภัยต่อชนิดใน TypeScript
ฉันกำลังพยายามเขียนคำจำกัดความชนิดสำหรับฟังก์ชันอรรถประโยชน์แบบนี้:
function getter<T>(field1, field2?, field3?) {
if (field3 !== undefined) {
return (obj: T) => obj[field1][field2][field3];
} else if (field2 !== undefined) {
return (obj: T) => obj[field1][field2];
} else {
return (obj: T) => obj[field1];
}
}
ดังนั้น เมื่อพิจารณาอินเทอร์เฟซต่อไปนี้:
interface Contact { phone: string; }
interface Author { name: string; contact: Contact; }
interface Book { id: number; author: Author; }
และตัวอย่างกรณี:
const lordOfTheRings: Book = { id: 1, author: { name: 'JRR Tolkien', contact: { phone: '222222' } } };
คุณสามารถใช้ getter<Book>('author', 'name') เพื่อให้ได้ฟังก์ชันซึ่งเมื่อถูกเรียกด้วยอาร์กิวเมนต์ lordOfTheRings จะคืนค่า JRR Tolkien
ดังนั้นฟังก์ชันข้างต้นทำงานได้ดี แต่ไม่ปลอดภัยตามประเภทข้อมูล ฉันต้องการที่จะสามารถเขียน getter<Book>('id') แล้วให้มันคอมไพล์ได้อย่างไม่มีปัญหา แต่ถ้าฉันเขียน getter<Book>('banana') ให้มันแสดงข้อผิดพลาดประเภท เพราะ banana ไม่ใช่คีย์ที่ถูกต้องของ Book ฉันเพียงต้องการให้มันทำงานได้ลึกไปถึงสามระดับ
ฉันเคยดูเรื่องนี้มาหลายครั้งแล้วและพยายามเขียนคำจำกัดความประเภทที่ใช้ได้ในทุกกรณี
สิ่งที่ฉันสามารถทําได้คือสิ่งนี้:
interface Getter<T> {
<K extends keyof T>(field1: K): (obj: T) => T[K];
<K extends keyof T, L extends keyof T[K]>(field1: K, field2: L): (obj: T) => T[K][L];
<K extends keyof T, L extends keyof T[K], M extends keyof T[K][L]>(field1: K, field2: L, field3: M): (obj: T) => T[K][L][M];
}
const bookGetter: Getter<Book> = getter;
ซึ่งฉันสามารถใช้ได้ดังนี้:
bookGetter('id') // No error
bookGetter('title') // Error
bookGetter('author')(lordOfTheRings).name; // No error
bookGetter('author', 'name'); // No error
bookGetter('author', 'age'); // Error
bookGetter('author', 'contact')(lordOfTheRings).phone; // No error
bookGetter('author', 'contact')(lordOfTheRings).email; // Error
bookGetter('author', 'contact', 'phone'); // No error
bookGetter('author', 'contact', 'email'); // Error
นี่คือพฤติกรรมที่สมบูรณ์แบบในแง่ของคอมไพเลอร์ แต่ฉันอยากจะสามารถระบุชนิดเป็นพารามิเตอร์ให้กับเก็ตเตอร์ มากกว่าที่จะต้องกำหนดอินสแตนซ์เฉพาะของเก็ตเตอร์แบบนี้
ฉันได้ลองใช้ลายเซ็นนี้กับฟังก์ชัน:
function getter<T,
K extends keyof T = keyof T,
L extends keyof T[K] = keyof T[K],
M extends keyof T[K][L] = keyof T[K][L]>(field1: K, field2?: L, field3?: M) {
ซึ่งในตอนแรกดูมีแนวโน้มดี แต่ล้มเหลวตามข้อกำหนดดังนี้:
getter<Book>('id'); // No error - correct
getter<Book>('title'); // Error - correct
getter<Book>('author')(lordOfTheRings).name; // Error - NOT correct (property name does not exist on number | Author)
getter<Book>('author', 'name'); // All the rest also error in the same way as this one (argument of type not assignable to parameter of type never)
กรุณาอย่ากังวลเกี่ยวกับประโยชน์ใช้สอยของฟังก์ชันนี้ - ในตอนนี้มันกลายเป็นเพียงการออกกำลังกายทางปัญญาในเรื่องการกำหนดประเภทเท่านั้น! เราจะทำได้ไหม?
คลิกที่นี่เพื่อไปยังตัวอย่าง TS Playground ที่มีสิ่งเหล่านี้
กำลังใช้ TypeScript 3.1.4.
คำตอบ #1
ปัญหาในวิธีที่คุณนิยาม getter ก็คือ Typescript ไม่รองรับการสรุปค่าพารามิเตอร์บางส่วน ดังนั้นเมื่อคุณเขียน getter<Book> พารามิเตอร์ชนิดที่เหลือจะไม่ถูกสรุปค่า, พวกมันจะใช้ค่าดีฟอลต์ตามที่กำหนดแทน ซึ่งหมายความว่า T[K] จะเป็นยูเนียนของชนิดของคุณสมบัติทั้งหมดของ T และอาจจะไม่มีคีย์ที่เหมือนกันเลย ดังนั้นจึงเกิดข้อผิดพลาดเกี่ยวกับ never ที่คุณเจออยู่
การอนุมานชนิดบางส่วนอาจถูกเพิ่มเข้าไปใน Typescript ในเวอร์ชัน 3.3 (ตาม PR นี้) แต่แม้กระนั้นคุณก็จะต้องเขียนบางอย่างเหมือน getter<Book, *> โดยเพิ่ม * สำหรับแต่ละพารามิเตอร์ชนิดที่คุณต้องการให้อนุมาน
วิธีแก้ปัญหาที่จะใช้งานได้ในตอนนี้ (และ IMO อ่านได้ดีกว่าจริงๆ) คือการใช้ currying ฟังก์ชัน (เช่น ฟังก์ชันที่ส่งคืนฟังก์ชัน) ด้วยวิธีนี้ คุณจะระบุประเภทเป้าหมายในการโทรครั้งแรก และปล่อยให้การอนุมานดูแลส่วนที่เหลือในการโทรครั้งที่สอง:
function getter<T>() {
function makeGetter<K extends keyof T>(field1: K): (obj: T) => T[K];
function makeGetter<K extends keyof T, L extends keyof T[K]>(field1: K, field2: L): (obj: T) => T[K][L];
function makeGetter<K extends keyof T, L extends keyof T[K], M extends keyof T[K][L]>(field1: K, field2: L, field3: M): (obj: T) => T[K][L][M];
function makeGetter(field1: keyof any, field2?: keyof any, field3?: keyof any) {
if (field2 !== undefined && field3 !== undefined) {
return (obj: any) => obj[field1][field2][field3];
} else if (field2 !== undefined) {
return (obj: any) => obj[field1][field2];
} else {
return (obj: any) => obj[field1];
}
}
return makeGetter;
}
// ---- Example Usage
interface Contact { phone: string; }
interface Author { name: string; contact: Contact; }
interface Book { id: number; author: Author; }
const bookGetter: Getter<Book> = getter;
const lordOfTheRings: Book =
{ id: 1, author: { name: 'JRR Tolkien', contact: { phone: '222222' } } };
getter<Book>()('id')(lordOfTheRings).name;
getter<Book>()('bones');
getter<Book>()('id'); // No error - correct
getter<Book>()('title'); // Error - correct
getter<Book>()('author')(lordOfTheRings).name; //ok
getter<Book>()('author', 'name'); // ok
getter<Book>()('author', 'age'); //error
getter<Book>()('author', 'contact')(lordOfTheRings).phone; // ok
getter<Book>()('author', 'contact')(lordOfTheRings).email; //error
getter<Book>()('author', 'contact', 'phone'); // ok
getter<Book>()('author', 'contact', 'email'); //error
