javascript - ตัวดึงค่าซ้อนแบบทั่วไปใน TypeScript
ฉันกำลังพยายามเขียนฟังก์ชัน TS ที่ดึงค่าลงไปในเชิงลึกจากวัตถุที่กำหนด วัตถุนั้นสามารถเป็นหนึ่งในหลายประเภท ดังนั้นฉันจึงใช้ generics อย่างไรก็ตาม TS แจ้งข้อผิดพลาด ดังนั้นฉันรู้สึกว่าฉันเข้าใจวิธีการทำงานของ generics ใน TS ผิด
interface BaseValueObject<T> {
value: T | null
}
type StringValue = BaseValueObject<string>
type NumberValue = BaseValueObject<number>
interface FormAData {
name: StringValue,
age: NumberValue
}
interface FormBData {
height: NumberValue
nickname: StringValue
}
interface FormA {
data: FormAData
}
interface FormB {
data: FormBData
}
type Form = FormA | FormB
const getFormValue =
<F extends Form, P extends keyof F['data']>(form: F, property: P) =>
form['data'][property]['value'] // Type 'P' cannot be used to index type 'FormAData | FormBData'
การใช้งานที่ต้องการ:
const formARecord: FormA = {
data: {
name: {
value: 'Joe'
},
age: {
value: 50
}
}
}
const joesAge = getFormValue(formARecord, 'age')
console.log(joesAge) // 50
สนามเด็กเล่น
วิธีแก้ปัญหา
นี่คือสิ่งที่ฉันทำในที่สุด คล้ายกับที่ @jered แนะนำในคำตอบของเขา:
สนามเด็กเล่น
โดยพื้นฐานแล้ว บทเรียนก็คือการทำให้ชัดเจนเกี่ยวกับ invariant ใดๆ ที่คุณมีใน typing ของคุณ ในกรณีของผม ผมไม่ได้บอกคอมไพเลอร์อย่างเป็นทางการว่า ทุก property ของ FormAData และ FormBData เป็นไปตาม interface เดียวกัน ผมสามารถทำได้โดยการขยายพวกมันจาก interface พื้นฐานนี้:
...
type Value = StringValue | NumberValue
interface BaseFormData {
[property: string]: Value
}
interface FormAData extends BaseFormData {
...
คำตอบ #1
คุณควรขยายการประกาศประเภททั่วไปของคุณไปยังอินเทอร์เฟซ "form" ด้วยตัวเอง
ในกรณีนี้ คุณจำเป็นต้องให้ TypeScript มีวิธีในการ 'อนุมาน' ว่าประเภทของคุณสมบัติ data ของ form จะเป็นแบบใด เพื่อให้ property สามารถเข้าถึงมันได้อย่างถูกต้อง
วิธีที่คุณเขียนในปัจจุบันทำให้เกิดข้อผิดพลาดเพราะคุณไม่สามารถใช้ keyof เพื่อดึงคุณสมบัติของประเภทยูเนียนได้ ลองพิจารณาตัวอย่างนี้:
type Foo = {
fooProperty: string;
}
type Bar = {
barProperty: string;
}
type Baz = Foo | Bar;
type Qux = keyof Baz; // type Qux = never
ประเภทของ Qux ควรจะเป็นอะไร? มันไม่สามารถเป็นคีย์ของสองประเภทที่แตกต่างกันในเวลาเดียวกันได้ ดังนั้นมันจึงกลายเป็นประเภท never ซึ่งไม่ค่อยมีประโยชน์สำหรับการเข้าถึงคุณสมบัติของวัตถุ
ในทางกลับกัน ลองพิจารณาว่าประเภท Form พื้นฐานของคุณเองเป็นประเภทแบบทั่วไป (generic) ที่คุณทราบว่าควรมีคุณสมบัติ data เสมอ แต่ประเภทเฉพาะของคุณสมบัติ data นั้นยังไม่ทราบจนกว่าจะถูกใช้งานจริง โชคดีที่คุณยังสามารถจำกัดบางด้านของ data เพื่อให้มั่นใจว่าการบังคับใช้โครงสร้างของมันทั่วทั้งแอปของคุณได้:
interface FormDataType {
[key: string]: StringValue | NumberValue;
};
interface Form<T extends FormDataType> {
data: T
};
จากนั้นเมื่อคุณเขียนรสชาติของ Form ที่มีการกำหนดประเภทที่เฉพาะเจาะจงมากขึ้นสำหรับคุณสมบัติ data คุณสามารถทำได้ดังนี้:
type FormA = Form<{
name: StringValue,
age: NumberValue
}>;
type FormB = Form<{
height: NumberValue
nickname: StringValue
}>;
ในทางหนึ่ง นี่ก็เหมือนกับการ “ขยาย” ประเภท แต่ในแบบที่ทำให้ TypeScript สามารถใช้ Form generic เพื่อ อนุมาน (ตามตัวอักษร) ประเภทของ data ในภายหลัง
ตอนนี้เราสามารถเขียนใหม่ประเภททั่วไปของฟังก์ชัน getFormValue() ให้ตรงกับประเภททั่วไปของ Form ในลายเซ็นต์ของฟังก์ชันได้ โดยในอุดมคติ ประเภทของค่าที่ฟังก์ชันส่งกลับจะสามารถสรุปได้อย่างสมบูรณ์เพียงจากพารามิเตอร์ของฟังก์ชันและตัวฟังก์ชันเอง แต่ในกรณีนี้ผมไม่พบวิธีที่ดีในการจัดโครงสร้างประเภททั่วไปเพื่อให้ทุกอย่างถูกสรุปโดยอัตโนมัติ ดังนั้นเราสามารถทำการแปลงประเภทของค่าที่ฟังก์ชันส่งกลับโดยตรง วิธีนี้มีประโยชน์คือ 1. ยังคงตรวจสอบว่า form["data"] มีอยู่และตรงกับโครงสร้าง FormDataType ที่เรากำหนดไว้ก่อนหน้านี้ และ 2. สามารถสรุปประเภท จริง ของค่าที่ส่งกลับจากการเรียก getFormValue() ทำให้ความมั่นใจในการตรวจสอบประเภทโดยรวมของคุณเพิ่มขึ้น
const getFormValue = <
F extends Form<FormDataType>,
P extends keyof F["data"]
>(
form: F,
property: P
) => {
return form["data"][property].value as F["data"][P]["value"];
}
สนามเด็กเล่น
แก้ไข: เมื่อพิจารณาเพิ่มเติมแล้ว generics ของ interface Form เองก็ไม่จำเป็นจริง ๆ คุณสามารถทำอย่างอื่นได้ เช่น ประกาศ interface Form พื้นฐาน แล้วจากนั้น extend มันกับแต่ละฟอร์มเฉพาะ:
interface FormDataType {
[key: string]: StringValue | NumberValue;
}
interface Form {
data: FormDataType
};
interface FormA extends Form {
data: {
name: StringValue;
age: NumberValue;
}
};
interface FormB extends Form {
data: {
height: NumberValue;
nickname: StringValue;
}
};
const getFormValue = <
F extends Form,
P extends keyof F["data"]
>(
form: F,
property: P
) => {
return form["data"][property].value as F["data"][P]["value"];
}
