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

javascript - TypeScript เจนเนอริก ข้อจำกัดสำหรับพารามิเตอร์ฟังก์ชันในอ็อบเจกต์


สิ่งที่ฉันต้องการทำให้สำเร็จ:

สมมติว่ามีการตั้งค่าที่มีประเภทแบบนี้:

type ExmapleConfig = {
    A: { Component: (props: { type: "a"; a: number; b: number }) => null };
    B: { Component: (props: { type: "b"; a: string; c: number }) => null };
    C: { Component: () => null };
};

ดังนั้น โดยทั่วไปแล้ว สิ่งที่มีรูปร่างแบบนี้:

type AdditionalConfigProps = {
    additionalConfigProp?: string;
    // + more additional props that don't have to be optional
};

type ReservedComponentProps = {
    reservedComponentProp: string;
};

type ComponentProps = ReservedComponentProps & Record<string, any>;

type Config = {
    [key: string]: {
        Component: (props: PropsShape) => JSX.Element;
    } & AdditionalConfigProps;
};

ฉันอยากจะแปลงการตั้งค่าประมาณนี้ แต่:

  • เก็บประเภทที่ชัดเจนสำหรับคีย์ ('A' | 'B' | 'C' แทน string)
  • สงวนประเภทที่เข้มงวดสำหรับ props ({ type: "a"; a: number; b: number } แทน Record<string, any>)
  • ตรวจสอบให้แน่ใจว่าฟังก์ชัน transform ยอมรับเฉพาะการตั้งค่าที่ถูกต้อง นั่นคือ:

    • มันมีคุณสมบัติ Component และคุณสมบัติอื่นทั้งหมดจาก AdditionalConfigProps พร้อมกับประเภทที่ถูกต้อง,
    • มันจะไม่ยอมรับคุณสมบัติเพิ่มเติมใด ๆ นอกเหนือจาก Component ที่กำหนดไว้และสิ่งที่อยู่ใน AdditionalConfigProps,
    • ฟังก์ชัน Component ต้องสามารถรับวัตถุที่คล้ายกับ ComponentProps เป็นอาร์กิวเมนต์ตัวแรกได้,

การเปลี่ยนแปลงอาจมีลักษณะดังนี้:

const config = {
    A: { Component: (props: { type: "a"; a: number; b: number }) => <div>abc</div> };
    B: { Component: (props: { type: "b"; a: string; c: number }) => <div>abc</div>  };
    C: { Component: () => <div>abc</div>  };
};

/*
    Let's say that it will extract Components, and wrap them
    with additional function so void will be returned instead of JSX
*/
const transformedConfig = transformConfig(config);

// typeof transformedConfig
type ResultType = {
    A: (props: { type: "a"; a: number; b: number }) => void;
    B: (props: { type: "b"; a: string; c: number }) => void;
    C: () => void;
};

กรุณาสังเกตว่า:

  • ประเภทฮาร์ดสำหรับปุ่ม 'A' | 'B' | 'C' ถูกเก็บไว้
  • ประเภทที่ยากสำหรับ 'props' ถูกเก็บรักษาไว้

วิธีที่ฉันลองทำ:

import React from "react";

type AdditionalConfigProps = {
    additionalConfigProp?: string;
};

type ReservedComponentProps = {
    reservedComponentProp: string;
};

const CORRECT_CONFIG = {
    A: {
        Component: (props: { type: "a"; a: number; b: number }) => null,
        additionalConfigProp: "abc"
    },
    B: { Component: (props: { type: "b"; a: string; c: number }) => null },
    C: { Component: (props: { reservedComponentProp: "c"; a: string }) => null },
    D: { Component: (props: {}) => null },
    E: { Component: () => null }
};

const BAD_CONFIG = {
    // Missing Component or other required config prop
    A: {},
    // Bad additionalConfigProp
    B: { Component: () => null, additionalConfigProp: 123 },
    // Bad Component
    C: { Component: 123 },
    // Bad component props type
    D: { Component: (props: boolean) => null },
    // Unexpected 'unknownProp'
    E: { Component: () => null, unknownProp: 123 },
    // Bad 'reservedProp'
    F: { Component: (props: { reservedProp: number }) => null }
};

function configParser<
    Keys extends string,
    ComponentPropsMap extends {
        [Key in Keys]: ReservedComponentProps & Record<string, any>;
    }
>(config: {
    [Key in Keys]: {
        Component: (props?: ComponentPropsMap[Keys]) => React.ReactNode;
    } & AdditionalConfigProps;
}) {
    /*
        TODO: Transform config.
        For now we want to make sure that TS is even able to 'see' it correctly.
    */
    return config;
}

/*
    ❌ Throws unexpected type error
*/
const result = configParser(CORRECT_CONFIG);

// Expected typeof result (what I'd want)
type ExpectedResultType = {
    A: {
        Component: (props: { type: "a"; a: number; b: number }) => null;
        additionalConfigProp: "abc";
    };
    B: { Component: (props: { type: "b"; a: string; c: number }) => null };
    C: { Component: (props: { reservedComponentProp: "c"; a: string }) => null };
    D: { Component: (props: {}) => null };
    E: { Component: () => null };
};

/*
    ❌ Should throw type errors, but not the ones it does
*/
configParser(BAD_CONFIG);

แน่นอนว่าฉันสามารถทำอะไรแบบนี้ได้:

function configParser<
    Config extends {
        [key: string]: {
            Component: (componentProps: any) => React.ReactNode;
        };
    }
>(config: Config) {
    return config;
}

// No type error, result type as expected
const result = configParser(CORRECT_CONFIG);

แต่ก็:

  • จะไม่ยืนยัน componentProps (บางที componentProps: Record<string, any> & ReservedComponentProps อาจได้ แต่ด้วยเหตุผลบางอย่างมันไม่ยอมรับ CORRECT_CONFIG)
  • จะอนุญาตให้มีคุณสมบัติการกำหนดค่าเพิ่มเติมใด ๆ

คำตอบ #1

นี่คือวิธีการที่เป็นไปได้หนึ่งวิธี:

type VerifyConfigElement<T extends AdditionalConfigProps &
{ Component: (props: any) => void }> =
  { [K in Exclude<keyof T, "Component" | keyof AdditionalConfigProps>]: never } &
  {
    Component: (
      props: Parameters<T["Component"]>[0] extends ComponentProps ? any : ComponentProps
    ) => void
  }

declare function transformConfig<
  T extends Record<keyof T, AdditionalConfigProps & { Component: (props: any) => void }>>(
    config: T & { [K in keyof T]: VerifyConfigElement<T[K]> }
  ): { [K in keyof T]: (...args: Parameters<T[K]["Component"]>) => void }

แนวคิดคือ:

  • ทำให้ transformConfig() เป็นแบบทั่วไปในประเภท T ของพารามิเตอร์ config;
  • จำกัด T ให้เป็นชนิดที่ค่อนข้างง่ายต่อการเขียนซึ่งไม่ปฏิเสธอินพุตที่ดี ในกรณีนี้คือ AdditionalConfigProps & {Component: (props: any) => void};
  • ตรวจสอบแต่ละคุณสมบัติของ T ที่คาดเดาได้อย่างละเอียดมากขึ้น โดยแมปมันจากตัวมันเอง T[K] ไปยังชนิดที่เกี่ยวข้อง VerifyConfigElement<T[K]> โดยที่ T[K] extends VerifyConfigElement<T[K]> ก็ต่อเมื่อมันเป็นข้อมูลนำเข้าที่ดีเท่านั้น;
  • คำนวณประเภทผลลัพธ์จาก T โดยแมปแต่ละคุณสมบัติของ T ไปเป็นประเภทฟังก์ชันซึ่งพารามิเตอร์ของมันถูกกำหนดโดยการเข้าถึงดัชนีในคุณสมบัติ Component ที่สอดคล้องกัน

ชนิด VerifyConfigElement<T> ตรวจสอบสองสิ่ง:

  • ว่า T ไม่มีคุณสมบัติใด ๆ ที่ไม่ได้กล่าวถึงอย่างชัดเจนใน AdditionalConfigProps (หรือ "Component", แน่นอน)... มันทำสิ่งนี้โดยการแมปคุณสมบัติเพิ่มเติมใด ๆ ให้มีประเภท never ซึ่งแทบจะแน่นอนว่าจะตรวจสอบประเภทไม่ผ่าน;
  • พารามิเตอร์ตัวแรกของเมธอด Component ของ T สามารถกำหนดค่าให้กับ ComponentProps ได้… มันทำเช่นนี้โดยการแมปไปที่ any ถ้าเป็นเช่นนั้น (ซึ่งจะสำเร็จ) และไปที่ ComponentProps ถ้าไม่ (ซึ่ง อาจ ล้มเหลว? ประเภทฟังก์ชันมีความแปรผันย้อนกลับในพารามิเตอร์อินพุตของพวกมัน ดังนั้นอาจมีกรณีขอบเขตบางอย่างอยู่ที่นี่)

มาทดสอบกันเถอะ:

const config = {
  A: { Component: (props: { type: "a"; a: number; b: number }) => <div>abc</div> },
  B: { Component: (props: { type: "b"; a: string; c: number }) => <div>abc</div> },
  C: { Component: () => <div>abc</div> }
};
// typeof transformedConfig
type ResultType = {
  A: (props: { type: "a"; a: number; b: number }) => void;
  B: (props: { type: "b"; a: string; c: number }) => void;
  C: () => void;
};////
const transformedConfig: ResultType = transformConfig(config);

ดูดี! และสำหรับ CORRECT_CONFIG และ BAD_CONFIG ของคุณ ตัวคอมไพเลอร์จะยอมรับและปฏิเสธตามลำดับ:

const okay = transformConfig(CORRECT_CONFIG); // okay
const bad = transformConfig(BAD_CONFIG); // error

ตามที่ต้องการ

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