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

การกำหนดฟังก์ชันเข้าถึงวัตถุเจเนอริกที่ปลอดภัยต่อชนิดใน 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

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