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

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"];
}