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

typescript - สร้างชนิดข้อมูลแบบแม็ปจากอาเรย์ของวัตถุทั่วไป


ฉันมีชนิดทั่วไปซึ่งเพิ่มคุณสมบัติเพิ่มเติมให้กับชนิดอื่น:

type ObjWithPath<T> = { [P in keyof T]: T[P] } & { _path: string }

ฉันมีฟังก์ชัน JS กล่องดำที่ต้องระบุประเภทการคืนค่า ซึ่งฟังก์ชันนี้รับอาร์เรย์ของประเภททั่วไปนี้เป็นพารามิเตอร์:

declare function blackBox(objs: ObjWithPath<any>[]): any; // <-- I'd like to create a return type given the array passed in

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

เมื่อมีอาร์เรย์อินพุตที่มีชนิดดังต่อไปนี้

const startingArray: ObjWithPath<any>[] = [
    { _path: "hello", prompt: "World" }, // ObjWithPath<{prompt: string}>
    { _path: "foo", bar: 4 },            // ObjWithPath<{bar: number}>
    { _path: "testing", value: 123 },    // ObjWithPath<{value: number}>
]

ประเภทค่าที่ส่งกลับของฟังก์ชันคาดว่าจะเป็น

{
  hello: { prompt: string },
  foo: { bar: number },
  testing: { value: number },
}

เป้าหมายคือด้วยวัตถุที่ส่งกลับมา ฉันควรจะสามารถทำสิ่งนี้ได้:

const returnObj = blackBox(startingArray)
console.log(returnObj.hello) // { prompt: "World" }
// or
console.log(returnObj["hello"]) // { prompt: "World" }

ในอีกมุมหนึ่ง วัตถุที่ คีย์ คือค่าของคุณสมบัติ _path ของแต่ละรายการในอาร์เรย์ และค่ามีประเภทเป็นพารามิเตอร์ทั่วไปของรายการอาร์เรย์นั้น

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

คำตอบ #1

ถ้าคุณต้องการให้คอมไพเลอร์ติดตามชนิดของข้อความตัวอักษร (string literal types) ของคุณสมบัติ _path ของ startingArray เพื่อให้สามารถใช้เป็นคีย์ของค่าที่ส่งกลับจาก blackBox() รวมถึงชนิดของแต่ละคุณสมบัติด้วย คุณก็ไม่สามารถระบุประเภทเป็น ObjWithPath<any>[] หรือแม้แต่ทูเพิลเช่น [ObjWithPath<{prompt: string}>, ObjWithPath<{bar: number}>, ObjWithPath<{value: number}>] การระบุประเภทของตัวแปรด้วยชนิด (ที่ไม่ใช่ union) จะเป็นการทิ้งข้อมูลรายละเอียดเพิ่มเติมจากตัวเริ่มต้นของมัน

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

const startingArray = [
  { _path: "hello", prompt: "World" },
  { _path: "foo", bar: 4 },
  { _path: "testing", value: 123 },
] as const; // <-- need this or the complier has no idea what strings _path are

/* const startingArray: readonly [{
    readonly _path: "hello";
    readonly prompt: "World";
}, {
    readonly _path: "foo";
    readonly bar: 4;
}, {
    readonly _path: "testing";
    readonly value: 123;
}] */

นั่นอาจเป็นข้อมูลมากกว่าที่คุณสนใจจะเก็บรักษาไว้ แต่ตอนนี้เราก็สามารถดำเนินการต่อได้อย่างน้อย


จากที่กล่าวมา เราต้องการเขียนประเภทสาธารณูปโภคทั่วไปเพื่อแปลงจากอาเรย์ของประเภทที่เข้ากันได้กับ ObjWithPath<?> เป็นประเภทของวัตถุผลลัพธ์ นี่คือตัวอย่างวิธีการทำ:

type ObjWithPathArrayToPlainObj<T extends readonly { _path: string }[]> =
  { [U in T[number] as U["_path"]]: Omit<U, "_path">}

ประเภทนี้จำกัดพารามิเตอร์ประเภทของมันให้เป็นอาร์เรย์ของวัตถุที่มีคุณสมบัติ _path แบบสตริง (จริง ๆ แล้วเป็นอาร์เรย์แบบ readonly ซึ่งสามารถรับอาร์เรย์ที่แก้ไขได้ด้วย) และมันใช้การแมประบุคีย์ใหม่เพื่อแปลงประเภทของสมาชิกอาร์เรย์ทั้งหมดเป็นคู่คีย์-ค่า เมื่อให้ประเภทที่คล้ายอาร์เรย์ T ประเภท T[number] คือยูเนียนของประเภทของสมาชิกของมัน (ซึ่งคือสิ่งที่คุณจะได้ถ้าคุณเข้าถึง T ด้วยคีย์ตัวเลข) สำหรับแต่ละสมาชิก U ในยูเนียนนั้น เราต้องการใช้ U["_path"] เป็นคีย์ (อีกครั้ง เข้าถึง U ด้วยคีย์ _path) และสำหรับค่า เราต้องการใช้ Omit<U, "_path"> ผ่านยูทิลิตี้ประเภท Omit สำหรับการลบคีย์หรือหลายคีย์ออกจากประเภท

นี่คือวิธีพื้นฐาน แม้ว่ามันมักจะสร้างผลลัพธ์ที่อ่านยากขึ้น ดังนั้นคุณอาจเปลี่ยนไปเป็น

type ObjWithPathArrayToPlainObj<T extends readonly { _path: string }[]> =
  { [U in T[number] as U["_path"]]: 
    { [K in keyof U as K extends "_path" ? never : K]: U[K] } 
  } & {} 

ซึ่งคำนวณค่าเทียบเท่ากับ Omit ด้วยตนเอง (โดยใช้การแมปคีย์เพื่อกรอง _path โดยใช้ never เป็นคีย์) และทำการ intersect กับชนิดอ็อบเจ็กต์ว่างเพื่อโน้มน้าวให้คอมไพเลอร์แสดงชนิดเป็น {x: {a: 1}, y: {b: 0}} แทน ObjWithPathArrayToPlainObj<[{_path: "x", a: 1}, {_path: "y", b: 0}]>.


ตอนนี้เราสามารถเขียน blackBox() เพื่อใช้การพิมพ์นั้น อาจจะเป็นแบบนี้:

const blackBox =
  <const T extends readonly { _path: string }[]>(arr: T) =>
    Object.fromEntries(arr.map(({ _path, ...rest }) => [_path, rest])) as
    ObjWithPathArrayToPlainObj<T>;

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

อย่างไรก็ตาม การพิมพ์นี้เพียงแค่รับอาร์เรย์ของประเภท T และส่งคืนค่าของประเภท ObjWithPathArrayToPlainObj<T> การนำไปใช้งานจริงใช้ Object.fromEntries() เพื่อประกอบอาร์เรย์ของคู่คีย์-ค่าเป็นวัตถุ และคู่คีย์-ค่าจะถูกสร้างจาก arr โดยการแม็ปไปยังคุณสมบัติ _path และส่วนที่เหลือของวัตถุผ่านการกำหนดค่าด้วยการแยกโครงสร้าง นั่นอาจเป็นวิธีสั้นที่สุดที่ฉันนึกออกในการเขียน แต่คุณสามารถขยายมันออกไปตามที่คุณต้องการ


โอเค ลองทดสอบมันกันเถอะ:

const resultObj = blackBox(startingArray);
/* const resultObj: {
    hello: { readonly prompt: "World"; };
    foo: { readonly bar: 4; };
    testing: { readonly value: 123; };
} */    
console.log(resultObj.hello.prompt.toUpperCase()) // "WORLD"

console.log(resultObj);
/* {
  "hello": { "prompt": "World" },
  "foo": { "bar": 4 },
  "testing": { "value": 123 }
} */

ดูดี ประเภทของ resultObj ตรงกับค่าผลลัพธ์อย่างแม่นยำ ดังนั้นคอมไพเลอร์จึงรู้ได้ เช่นว่า resultObj.hello มีคุณสมบัติ prompt ที่มีค่าเป็นสตริง

ตัวแก้ไข const T ทำให้สิ่งต่อไปนี้สามารถทำงานได้ตามที่ต้องการด้วยอาร์เรย์ลิตเทอรัลแบบอินไลน์:

const resultObj2 = blackBox([
  { _path: "hello", prompt: "World" },
  { _path: "foo", bar: 4 },
  { _path: "testing", value: 123 },
]);
/* const resultObj2: {
    hello: { readonly prompt: "World"; };
    foo: { readonly bar: 4; };
    testing: { readonly value: 123; };
} */

หากคุณลบมันออก คุณจะได้ประเภทที่กว้างเกินไปพร้อมสัญลักษณ์ดัชนีสตริงเนื่องจากคอมไพเลอร์จะไม่ติดตามว่า _path เป็นอะไรนอกจาก string:

const blackBox = <T extends ⋯
// ------------> ^^^^ <--- removed const modifier

const resultObj2 = blackBox([
  { _path: "hello", prompt: "World" },
  { _path: "foo", bar: 4 },
  { _path: "testing", value: 123 },
]);
/* const resultObj2: {
  [x: string]: {
      prompt: string;
      bar?: undefined;
      value?: undefined;
  } | {
      bar: number;
      prompt?: undefined;
      value?: undefined;
  } | {
      value: number;
      prompt?: undefined;
      bar?: undefined;
  };
} */

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