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

javascript - การเก็บรักษาเจนเนอริกในประเภทแมปของ TypeScript


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

type InputRef<T> = {
  current: T,
};
type OutputRef<T> = {
  current?: T,
};

class Original {
  increment(input: InputRef<number>, output: OutputRef<number>) {
    const { current: inValue } = input;
    
    output.current = inValue + 1;
  }
}

type Mapper<Fn> = Fn extends (input: InputRef<infer U>, output: OutputRef<infer V>) => unknown ? (input: U) => V : never;
type MyProxyGeneratedByThirdPartyJs = { [FnName in keyof Original]: Mapper<Original[FnName]> };

declare const proxy: MyProxyGeneratedByThirdPartyJs;

const result = proxy.increment(3); // 4

อย่างไรก็ตาม ตัวแม็ปไม่ทำงานเมื่อฮันเดิลเลอร์เกี่ยวข้องกับประเภททั่วไป เช่น,

class Original {
  toBox<T>(input: InputRef<T>, output: OutputRef<{ boxed: T }>) {
    const { current: inValue } = input;

    output.current = { boxed: inValue };
  }
}

โดยใช้วิธีเดียวกันข้างต้น ประเภทของ proxy จะเกี่ยวข้องกับ unknown เท่านั้น และข้อมูลเจเนอริก T จะหายไป

ในอุดมคติ ฉันต้องการให้ proxy เป็นชนิด

{
  toBox<T>(input: T): { boxed: T },
}

แทนที่จะ

{
  toBox(input: unknown): { boxed: unknown },
}

มีวิธีใดบ้างในการทำให้สิ่งนี้สำเร็จ?

คำตอบ #1

สิ่งนี้ยังไม่สามารถทำได้ใน TypeScript ในการที่จะบอกสิ่งที่คุณกำลังทำกับฟังก์ชันทั่วไปในระดับประเภท คุณจะต้องใช้ higher kinded types ตามที่ร้องขอใน microsoft/TypeScript#1213 แต่สิ่งเหล่านี้ไม่ได้รับการสนับสนุนโดยตรง การกำหนดประเภทเงื่อนไขของ Mapper<Fn> จะอนุมาน U เป็นประเภทอินพุตและ V เป็นประเภทเอาต์พุต แต่ตัวคอมไพเลอร์ไม่มีความสามารถในการเห็นหรือแสดงความสัมพันธ์ระดับสูงระหว่างพวกมันเมื่อ Fn เป็นแบบทั่วไป

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

ถึงอย่างนั้น เพื่อแสดงให้เห็นว่ามีความสามารถบางส่วนในการทำสิ่งนี้ นี่คือวิธีที่ใครบางคนอาจจะลองทำ:

function proxySingleFunction<U, V>(
    f: (input: InputRef<U>, output: OutputRef<V>) => any
): (input: U) => V {
    return function (input: U) {
        const o: OutputRef<V> = {};
        f({ current: input }, o);
        const ret = o.current;
        if (ret === undefined) throw new Error("OH NO");
        return ret;
    }
}

ประเภทของ proxySingleFunction() คือ

// function proxySingleFunction<U, V>(
//   f: (input: InputRef<U>, output: OutputRef<V>) => any
// ): (input: U) => V

ซึ่งดูคล้ายกับสิ่งที่คุณกำลังทำกับ Mapper<Fn> จากนั้น หากคุณ เรียก proxySingleFunction() มันจะสร้างผลลัพธ์ของประเภทที่เกี่ยวข้อง:

const increment = proxySingleFunction(Original.prototype.increment);
// const increment: (input: number) => number

const toBox = proxySingleFunction(Original.prototype.toBox);
// const toBox: <T>(input: T) => { boxed: T; }

คุณสามารถเห็นได้ว่า toBox เป็นแบบทั่วไป ตามที่ต้องการ จากนั้นคุณสามารถรวมฟังก์ชันเอาต์พุตเหล่านี้ไว้ในวัตถุ proxy เดียวและใช้งานได้:

const proxy = {
    increment, toBox
}

console.log(proxy.increment(1).toFixed(1)) // "2.0"
console.log(proxy.toBox("a").boxed.toUpperCase()) // "A"

นั่นก็ดีและมันใช้ได้ แต่สิ่งนี้ต้องให้คุณสร้าง JavaScript สำหรับแต่ละเมธอดที่คุณต้องการแปลง หากคุณมีออบเจ็กต์ที่ถูกแปลงเช่นนั้นจากบุคคลที่สามอยู่แล้วและเพียงต้องการแสดงประเภทของมัน สิ่งที่ใกล้เคียงที่สุดที่คุณทำได้คือการหลอกตัวคอมไพเลอร์ผ่านการระบุประเภทเพื่อให้คิดว่าคุณกำลังทำการแปลงอยู่ ทั้งที่จริง ๆ แล้วคุณไม่ได้ทำ:

// pretend this function exists
declare const psf: <U, V>(
    f: (input: InputRef<U>, output: OutputRef<V>) => any
) => (input: U) => V;

// pretend you're using it to create a proxy object
const myProxyType = (true as false) || {
    increment: psf(Original.prototype.increment),
    toBox: psf(Original.prototype.toBox)
}

// get the pretend type of that object
type MyProxyGeneratedByThirdPartyJs = typeof myProxyType;
/* type MyProxyGeneratedByThirdPartyJs = {
  increment: (input: number) => number;
  toBox: <T>(input: T) => { boxed: T; };
} */

ฉันไม่เห็นว่านี่เป็นชัยชนะครั้งใหญ่เหนือการเขียนประเภทเหล่านี้ด้วยมือในตอนแรก ดังนั้นฉันก็ไม่แน่ใจว่าฉันจะแนะนำมัน มันเป็นเพียงสิ่งที่ใกล้เคียงที่สุดที่ฉันสามารถจินตนาการได้กับสิ่งที่คุณต้องการกับภาษาตามที่มันเป็นอยู่ในปัจจุบัน

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