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 ไปยังโค้ด
