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

angular - วิธีสร้างอ็อบเจ็กต์ตามชนิดและส่งค่ากลับสำหรับแต่ละคีย์เพียงค่าสตริงของคีย์แบบวนซ้ำ


ให้ฉันอธิบาย ฉันมีกรณีการใช้งานดังต่อไปนี้: ใน Angular ฉันกำลังใช้คอมโพเนนต์ตาราง UI ซึ่งมีลักษณะประมาณดังนี้

<my-table [values]="myTableValues">
  <my-column field="firstColumn" />
</my-table>

myTableValues มีชนิดสำหรับแต่ละแถว (เรียกมันว่า myTableValuesRowType) ซึ่งถูกสร้างขึ้นผ่านตัวสร้างโค้ดของ Swagger อย่างที่คุณเห็น field รับค่าเป็นสตริง ซึ่งทำให้ความเข้มงวดของชนิดข้อมูลน้อยลง

ฉันคิดวิธีแก้ปัญหาที่สร้างวัตถุของชนิดเดียวกันโดยมีคุณสมบัติเหมือนกันและคืนค่าค่าเป็นสตริงของแต่ละคีย์ ดังนั้นโดยพื้นฐานก็เป็นประมาณนี้:

[Key]: KeyString

นี่คือโค้ดของฉัน:

type NonNullableKeys<T> = {
  [K in keyof T]: T[K] extends null | undefined ? never : K;
}[keyof T];

export type PropertiesOf<T> = Record<NonNullableKeys<T>, string>;

export function getTypeProperties<T>(): PropertiesOf<T> {
  return new Proxy({}, {
    get: (_target, prop: string) => prop,
  }) as PropertiesOf<T>;
}

ตัวอย่าง Typescript Playground

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

//component.ts
readonly rowProperties = getTypeProperties<myTableValuesRowType>();

//component.html
<my-table [values]="myTableValues">
  <my-column [field]="rowProperties.firstColumn" />
</my-table>

ด้วยวิธีนี้ หากชนิดที่สร้างขึ้นมีการเปลี่ยนแปลงคีย์ของสมบัติ ผมจะสามารถตรวจสอบได้ทันที

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

ตัวอย่าง :

type myTableValuesRowType = {
    firstColumn: number;
    secondColumn: string;
    thirdColumn: number[];
    fourthColumn: boolean;
    subrow: {
        firstSubrowColumn: string;
        secondSubrowColumn: number;
    }
}

const rowProperties = getTypeProperties<myTableValuesRowType>();

console.log(rowProperties.firstColumn); //returns "firstColumn"
console.log(rowProperties.subrow.firstSubrowColumn); //returns undefined & TS-Error: Property 'firstSubrowColumn' does not exist on type 'string'. expecting to return "subrow.firstSubrowColumn"

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

เพื่อให้ทราบ ฉันไม่สามารถเปลี่ยนไลบรารีคอมโพเนนต์ส่วนติดต่อผู้ใช้ได้...

ฉันจะขอบคุณสำหรับความช่วยเหลือหรือข้อมูลเชิงลึกใดๆ!

อัปเดต: ฉันเจอบางสิ่งที่คล้ายกันมาก อย่างไรก็ตามมันจะแปลงเป็นฟังก์ชันและมันไม่ได้ดูดีเท่าจะเป็นวัตถุ

อัปเดตอีกครั้ง: ฉันลืมบอกไปว่าฉันคาดว่า rowProperties.subrow.firstSubrowColumn จะส่งกลับ subrow.firstSubrowColumn ไม่ใช่แค่คีย์ลูก firstSubrowColumn

คำตอบ #1

ชนิดเช่น MyTableValuesRowType จะถูกลบเมื่อ TypeScript ถูกคอมไพล์เป็น JavaScript ซึ่งหมายความว่าเมื่อคุณเรียก getTypeProperties<MyTableValuesRowType>() การเรียกใช้งานในเวลารันไทม์จะเหมือนกับ getTypeProperties() และถ้าคุณเรียก getTypeProperties<SomeOtherRowType>() ก็จะกลายเป็น getTypeProperties() ในเวลารันไทม์เช่นกัน ดังนั้นการทำงานของ getTypeProperties() จึงไม่สามารถรู้ได้ว่าคุณกำลังถามถึงชนิดใด สิ่งที่มันทำต้องเป็น อิสระ จากชนิดใน TypeScript

นั่นหมายความว่าสิ่งที่คุณกำลังขอเป็นไปไม่ได้ตามที่กล่าวไว้ ไม่มีทางที่ rowProperties.firstColumn จะเป็น string ในขณะที่ rowProperties.subrow เป็น object ทั้งคู่จะต้องเป็น string หรือเป็น object คุณต้องเลือกว่าจะ: ยอมแพ้โดยสิ้นเชิง; หรือปรับปรุงโค้ดของคุณใหม่เพื่อส่งบางอย่างที่เป็น schema object ซึ่งมีอยู่ในขณะ runtime แต่แทนรูปแบบของ interface; หรือคุณสามารถเปลี่ยนพฤติกรรมที่ตั้งใจไว้ของ getTypeProperties() เพื่อให้สามารถทำงานอย่างไม่ขึ้นกับประเภทแต่เพียงพอที่จะบรรลุเป้าหมายของคุณ

เป็นตัวอย่างของข้อหลัง มาทำให้ rowProperties.firstColumn และ rowProperties.subrow เป็นวัตถุทั้งคู่ และถ้าคุณต้องการชื่อคีย์จากนั้น คุณสามารถเข้าถึงวัตถุด้วยคีย์พิเศษ เพื่อไม่ให้รบกวนคีย์ที่มีอยู่แล้ว ผมขอแนะนำให้เราใช้ symbol:

const keyName = Symbol("keyName");

เมื่อเราทำสิ่งนั้นแล้ว เราสามารถเขียน getTypeProperties() เพื่อให้มันส่งกลับวัตถุเสมอ ซึ่งวัตถุนั้นจะให้สตริงที่ต้องการเมื่อคุณเข้าดัชนีด้วย keyName และในกรณีอื่น ๆ จะส่งกลับวัตถุอีกชิ้นหนึ่งของประเภทเดียวกัน เช่นนี้:

function getTypeProperties<T extends object>(
  key?: string
): { [K in keyof T]: PropertiesOf<T[K]> } {
  return new Proxy({}, {
    get: (_target, prop: string | symbol) =>
      typeof prop === "symbol" ? key :
        getTypeProperties(typeof key === "string" ? key + "." + prop : prop)
  }) as any
}

ที่นี่ผมได้เพิ่มพารามิเตอร์ตัวเลือกใน getTypeProperties() ซึ่งใช้ติดตามคีย์ที่ต้องการส่งกลับที่พร็อพเพอร์ตี keyName สำหรับการเรียกในระดับบนสุด มันก็เพียง undefined เพราะไม่มีชื่อคีย์ในระดับนั้น ภายใน getter ของ Proxy มันแค่ตรวจสอบว่าพร็อพเพอร์ตีนั้นเป็นสัญลักษณ์หรือไม่ (คุณ สามารถ ทำให้มันตรวจสอบเฉพาะ keyName ได้ แต่คุณต้องตัดสินใจว่าต้องการแสดงสตริงอะไรสำหรับคีย์สัญลักษณ์ และนั่นอยู่นอกขอบเขตที่นี่) ถ้ามันเป็นสัญลักษณ์ คุณจะได้รับคีย์ที่ถูกส่งเข้ามา มิฉะนั้นมันเป็นคีย์สตริงและเราจะส่งค่าผลลัพธ์จากการเรียกใหม่ของ getTypeProperties() ซึ่งจะต่อชื่อพร็อพเพอร์ตีปัจจุบันเข้ากับเส้นทางที่มีอยู่ โดยแทรกจุดระหว่างกลางหากเส้นทางของแม่ไม่ใช่ undefined.


ดังนั้นพฤติกรรมจึงดูเหมือน

type MyTableValuesRowType = {
  firstColumn: number;
  secondColumn: string;
  thirdColumn: number[];
  fourthColumn: boolean;
  subrow: {
    firstSubrowColumn: string;
    secondSubrowColumn: number;
  }
}

const rowProperties = getTypeProperties<MyTableValuesRowType>();
console.log(rowProperties.firstColumn[keyName]); // returns "firstColumn"
console.log(rowProperties.subrow.firstSubrowColumn[keyName]); // returns "subrow.firstSubrowColumn"

IDE ของ TypeScript ของคุณจะรู้ว่าคุณสมบัติใดที่คาดว่าจะมีอยู่ (เมื่อเรานำ PropertiesOf<> ไปใช้งานด้านล่าง) แต่จงจำไว้ว่าในเวลารันไทม์ มันไม่สนใจจริงๆ แท้จริงแล้วคุณสามารถเรียกใช้งานด้วยชื่อคีย์แบบสุ่มและรับผลลัพธ์ในเวลารันไทม์ได้:

console.log(rowProperties.foo.bar.baz[keyName]); // returns "foo.bar.baz"
//                        ~~~
// Property 'foo' does not exist on type '⋯'.(2339)

ประโยชน์ของการพิมพ์ที่นี่คือเพื่อทำให้บรรทัดด้านบนเป็นข้อผิดพลาดใน TypeScript ในเวลาคอมไพล์ แม้ว่าจะไม่มีอะไร 'แย่' เกิดขึ้นในเวลารันไทม์


โอเค ตอนนี้... เราจะสามารถใช้งาน PropertiesOf<T> ได้อย่างไร? วิธีการหนึ่งดูเหมือนว่าจะเป็นดังนี้:

type PropertiesOf<T> = T extends object ?
  T extends readonly any[] ? { [keyName]: string } :
  { [keyName]: string } & { [K in keyof T]: PropertiesOf<T[K]> } :
  { [keyName]: string };

มันเป็นประเภทตามเงื่อนไขที่ตรวจสอบว่า T เป็นอ็อบเจ็กต์ที่ไม่ใช่อาร์เรย์หรือไม่ (อาร์เรย์เป็นอ็อบเจ็กต์ใน JS/TS ดังนั้นคุณต้องตรวจสอบ) หากเป็นเช่นนั้น มันจะระบุว่ามีสมบัติ string ที่คีย์สัญลักษณ์ keyName จากนั้นทำการแมปผ่านสมบัติของ T เพื่อใช้ PropertiesOf กับแต่ละสมบัติแบบเรียกซ้ำ และรวมประเภทรวมกัน (ดังนั้นมันจึงมีทั้งคีย์ keyName และคีย์เดิมทั้งหมดด้วย) มิฉะนั้น ถ้าเป็นอาร์เรย์ หรือไม่ใช่อ็อบเจ็กต์ มันจะมีเพียงสมบัติ string เดียวที่คีย์สัญลักษณ์ keyName เท่านั้น

นั่นใช้ได้ แต่จะสร้าง IntelliSense types ที่มีการรวมกันหลายชั้น ผมมักจะชอบ "ทำให้สวย" มันด้วยอะไรบางอย่างเช่น:

type Id<T> = T extends any ? { [K in keyof T]: T[K] } : never

type PropertiesOf<T> = T extends object ?
  T extends readonly any[] ? { [keyName]: string } :
  Id<{ [keyName]: string } & { [K in keyof T]: PropertiesOf<T[K]> }> :
  { [keyName]: string };

โดยที่ Id<T> แค่รวม T เข้าเป็นชนิดเดียว ตอนนี้ สังเกตว่าลายเซ็นการเรียกของ getTypeProperties() คือ

declare function getTypeProperties<T extends object>(
  key?: string
): { [K in keyof T]: PropertiesOf<T[K]> };

ซึ่งหมายความว่าชนิดของวัตถุระดับบนสุดที่ส่งกลับ ไม่มี คุณสมบัติ keyName (อีกครั้ง นั่นเป็นเพียงชนิดของ TypeScript เท่านั้น พฤติกรรมในเวลารันไทม์ไม่ได้รู้เกี่ยวกับสิ่งนั้น) ดังนั้นตอนนี้ ถ้าเราดูที่ชนิดของการเรียกใช้นี้:

const rowProperties = getTypeProperties<MyTableValuesRowType>();
/* const rowProperties: {
    [keyName]: string;
    firstColumn: {
        [keyName]: string;
    };
    secondColumn: {
        [keyName]: string;
    };
    thirdColumn: {
        [keyName]: string;
    };
    fourthColumn: {
        [keyName]: string;
    };
    subrow: {
        [keyName]: string;
        firstSubrowColumn: {
            [keyName]: string;
        };
        secondSubrowColumn: {
            [keyName]: string;
        };
    };
} */

คุณสามารถเห็นโครงสร้างแบบวนซ้ำที่เราต้องการ แต่ละคุณสมบัติของ rowProperties เป็นวัตถุที่มีคุณสมบัติที่มีค่า string อยู่ที่คีย์ keyName และคุณสมบัติ subrow มีคุณสมบัติเพิ่มเติมที่ firstSubrowColumn และ secondSubrowColumn

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