วิธีการดึงค่าของอ็อบเจ็กต์เป็นชนิดลิตเทอรัลยูเนียนในไทป์สคริปต์
ฉันกำลังดำเนินการฟังก์ชัน groupBy และมันก็เป็นประมาณนี้:
export const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
objArr: T[],
property: U,
): { [key in T[U]]: T[] } => objArr
.reduce((memo, x) => {
const value = x[property];
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x);
return memo;
}, {} as { [key in T[U]]: Array<T> });
ฉันรู้ว่าการพิมพ์นั้นผิดพลาด ฉันก็ลองปรับมันมากเกินไปเช่นกัน:
const data = [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' },
{ name: 'snake', value: 2, group: 'animal' },
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' },
];
const result = groupBy(data, 'group')
{
item: [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' }
],
animal: [ { name: 'snake', value: 2, group: 'animal' } ],
car: [
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' }
]
}
นี่คือชนิดที่ปลอดภัยสูงสุดที่ฉันสามารถหาได้สำหรับสิ่งนี้ และฉันยังคงพบข้อผิดพลาดบางอย่างใน T[U].
Type 'T[U]' is not assignable to type 'string | number | symbol'.และประเภทการส่งค่ากลับเป็นดังนี้:
const result: {
[x: string]: {
name: string;
value: number;
group: string;
}[];
}
มีวิธีใดที่ฉันสามารถได้ประเภทค่าตอบแทนแบบนี้หรือไม่:
const result: {
item: {
name: string;
value: number;
group: string;
}[];
animal: {
name: string;
value: number;
group: string;
}[];
car: {
name: string;
value: number;
group: string;
}[];
}
ถ้าฉันสามารถดึงค่าของวัตถุอินพุตทั้งหมดของคีย์ที่กำหนดออกมาเป็นตัวอักษรตรง ๆ ได้เหมือน:
const test = [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' },
{ name: 'snake', value: 2, group: 'animal' },
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' },
] as const ;
type ValueAtKey = (typeof test)[number]['group']; // "item" | "animal" | "car"
แต่ฉันจะทำ const asertion ภายในฟังก์ชันเจนเนอริกได้อย่างไร?
ทำสิ่งแบบนี้:
type SomeMagicType<T extends Record<string, unknown>[], U extends keyof T[number]> = T[number][U];
export const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
objArr: T[],
property: U,
): { [key in SomeMagicType<T, U>]: T[] } => objArr
.reduce((memo, x) => {
const value = x[property];
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x);
return memo;
}, {} as { [key in SomeMagicType<T, U>]: Array<T> });
ถ้าเป็นไปไม่ได้ งั้นฉันจะลบข้อผิดพลาดข้างต้นได้อย่างไร ฉันไม่เข้าใจว่าเหตุใด Type 'T[U]' is not assignable to type 'string | number | symbol' ถ้า T extends Record<string, unknown>
TSPlayground
คำตอบ #1
ก่อนอื่น หากคุณต้องการให้ประเภทผลลัพธ์ของ groupBy มีคีย์เฉพาะอย่าง item, animal, และ car คุณจะต้องทำบางอย่างเหมือนกับการยืนยันแบบ const เมื่อคุณเริ่มต้น data โดยทั่วไป คอมไพเลอร์จะอนุมานประเภทของสมบัติที่มีค่าเป็นสตริงให้เป็น string ไม่ใช่ประเภทสตริงลิเทอรัลเฉพาะ ดังนั้นประเภทของ x ใน const x={a: "b"} จะถูกอนุมานเป็น {a: string} ไม่ใช่ {a: "b"} เนื่องจาก "item", "animal" และ "car" เป็นสมบัติที่มีค่าเป็นสตริงของ data คุณจะต้องทำงานบางอย่างเพื่อรักษาค่าของพวกมัน วิธีที่ง่ายที่สุดในการทำคือ:
const data = [
{ name: 'corn cob', value: 17, group: 'item' },
{ name: 'Dirty toilet', value: 6, group: 'item' },
{ name: 'snake', value: 2, group: 'animal' },
{ name: 'tesla', value: 17, group: 'car' },
{ name: 'gurgel', value: 23, group: 'car' },
] as const;
แม้ตอนนี้คอมไพเลอร์จะจำข้อมูลเกี่ยวกับโครงสร้างของ data ได้ค่อนข้างมาก:
/* const data: readonly [{
readonly name: "corn cob";
readonly value: 17;
readonly group: "item";
}, {
readonly name: "Dirty toilet";
readonly value: 6;
readonly group: "item";
}, {
readonly name: "snake";
readonly value: 2;
readonly group: "animal";
}, {
readonly name: "tesla";
readonly value: 17;
readonly group: "car";
}, {
readonly name: "gurgel";
readonly value: 23;
readonly group: "car";
}] */
และคุณสนใจเพียงประเภทตัวอักษรของ group จริงๆ คุณสามารถทำอะไรที่ซับซ้อนกว่านี้ได้ เช่น
const data = [
{ name: 'corn cob', value: 17, group: 'item' as const },
{ name: 'Dirty toilet', value: 6, group: 'item' as const },
{ name: 'snake', value: 2, group: 'animal' as const },
{ name: 'tesla', value: 17, group: 'car' as const },
{ name: 'gurgel', value: 23, group: 'car' as const },
];
ซึ่งส่งผลให้
/* const data: ({
name: string;
value: number;
group: "item";
} | {
name: string;
value: number;
group: "animal";
} | {
name: string;
value: number;
group: "car";
})[] */
ซึ่งผมว่าไม่น่าเกลียดเท่าไหร่ นั่นขึ้นอยู่กับคุณ
อย่างไรก็ตาม เมื่อคุณมี data ที่มีประเภทกำหนดชัดเจนเพียงพอแล้ว เราสามารถให้ groupBy มีลายเซ็นการเรียกใช้ที่ใช้งานมันได้อย่างถูกต้อง มาลองเริ่มกัน:
declare const groupBy:
<T extends Record<string, PropertyKey>, K extends keyof T>(
objArr: readonly T[],
property: K,
) => Record<T[K], T[]>
นี่คล้ายกับเวอร์ชันของคุณ (ยกเว้น readonly T[] ซึ่งยืดหยุ่นมากกว่า T[] และการเปลี่ยนชื่อจาก U เป็น K ที่เป็นที่นิยมมากกว่าสำหรับ "key") ความแตกต่างที่สำคัญที่นี่คือผมได้เปลี่ยนประเภทของค่า T จาก unknown เป็น PropertyKey ซึ่งเป็นประเภทที่กำหนดโดยไลบรารีมาตรฐานเทียบเท่ากับ string | number | symbol ซึ่งเป็นประเภทที่สามารถใช้เป็นคีย์ได้ ดังนั้นตอนนี้คอมไพเลอร์จะรู้ว่า T[K] เป็นประเภทที่คล้ายคีย์ และสามารถใช้แบบนั้นใน Record<T[K], T[]> ได้
แน่นอนว่าเราไม่ได้ต้องการจะบอกจริง ๆ ว่า ทุก property ใน T ต้องเป็นแบบ keylike; เราเพียงแค่อยากจำกัด property ที่ key K ให้เป็นชนิดนั้น ดังนั้นเราสามารถเขียนลายเซ็นการเรียกใหม่ได้ดังนี้:
declare const groupBy:
<T extends Record<K, PropertyKey>, K extends keyof T>(
objArr: readonly T[],
property: K,
) => Record<T[K], T[]>
ตอนนี้ T ขึ้นอยู่กับ K เราสามารถผ่อนปรนข้อจำกัดที่ว่า K extends keyof T เป็นเพียง K extends PropertyKey เนื่องจากข้อจำกัดใน T ตอนนี้รับประกันว่า K เป็นคีย์ แต่เรื่องนั้นขึ้นอยู่กับคุณ
อย่างไรก็ตาม ตอนนี้สิ่งนี้จะทำงาน แต่ประเภทที่ถูกยืนยันด้วย const ขนาดใหญ่นั้นจะย้อนกลับมาหาเราเมื่อเรามาดูประเภทของผลลัพธ์:
const resultOrig = groupBy(data, 'group');
/* const resultOrig: Record<"item" | "animal" | "car", ({
readonly name: "corn cob";
readonly value: 17;
readonly group: "item";
} | {
readonly name: "Dirty toilet";
readonly value: 6;
readonly group: "item";
} | {
readonly name: "snake";
readonly value: 2;
readonly group: "animal";
} | {
readonly name: "tesla";
readonly value: 17;
readonly group: "car";
} | {
readonly name: "gurgel";
readonly value: 23;
readonly group: "car";
})[]> */
ชนิดนั้นถูกต้อง แต่คุณกล่าวว่าคุณต้องการบางอย่างที่ไม่เฉพาะเจาะจงสำหรับค่าของคุณสมบัติ คุณสามารถให้คอมไพเลอร์คำนวณชนิดดังกล่าวได้ แต่ไม่ใช่เรื่องง่าย:
type WidenLiteral<T> =
T extends string ? string :
T extends number ? number :
T extends boolean ? boolean :
T;
declare const groupBy: <T extends Record<K, PropertyKey>, K extends PropertyKey>(
objArr: readonly T[], property: K) => Record<T[K], {
[P in PropertyKey & keyof T]: WidenLiteral<T[P]>;
}[]> */
สิ่งที่ผมทำที่นั่นคือเขียนชนิด WidenLiteral<T> ซึ่งจะเปลี่ยนชนิดหนึ่งให้เป็นเวอร์ชันที่กว้างขึ้น โดยที่ชนิดลิตเติ้ล string, number, หรือ boolean จะถูกกว้างขึ้นเป็น string, number, และ boolean ตามลำดับ ดังนั้น WidenLiteral<"a" | Date> จะกลายเป็น string | Date และ WidenLiteral<1 | 2 | 3 | 4> จะกลายเป็น number
แล้ว แทนที่จะส่งกลับอาร์เรย์ของ T[] ผมส่งกลับอาร์เรย์ {[P in PropertyKey & keyof T]: WidenLiteral<T[P]>} สิ่งที่สิ่งนี้ทำคือเอาประเภท T และถ้ามันเป็นยูเนียน มันจะรวมเข้าด้วยกันเป็นประเภทเดียว (และลืมคุณสมบัติใด ๆ ที่ไม่มีในสมาชิกใด ๆ ของยูเนียน) จากนั้นมันจะขยายคุณสมบัติลิเทอรัลด้วย WidenLiteral ดังนั้นถ้าอินพุตคือ {a: 0, b: "a", c: true} | {a: 1, b: "b", d: false} ผลลัพธ์คือ {a: number, b: string}
โปรดทราบว่าสิ่งนี้จะเกิดขึ้นโดยอัตโนมัติเมื่อประเภทที่แมปกับ T ไม่เป็นโฮโมมอร์ฟิค ดังนั้น {[P in keyof T]: 0} เป็นโฮโมมอร์ฟิคและจะเปลี่ยน {a: 1, c: 1} | {a: 2, d: 2} เป็น {a: 0, c: 0} | {a: 0, d: 0} แต่ {[P in (PropertyKey & keyof T)]: 0} ไม่เป็นโฮโมมอร์ฟิคและจะเปลี่ยน {a: 1, c: 1} | {a: 2, d: 2} เป็น {a: 0}.
อย่างไรก็ตาม ลายเซ็นของการเรียกนี้จะเปลี่ยนความยุ่งเหยิงของประเภทของ data ให้กลายเป็นสิ่งที่น่าพอใจมากขึ้น:
const result = groupBy(data, 'group');
/* const result: Record<"item" | "animal" | "car", {
group: string;
name: string;
value: number;
}[]> */
เยี่ยม!
โอ้ แต่แน่นอนว่าการใช้งานของ groupBy จะต้องมีการยืนยันประเภทอย่างน้อยหนึ่งครั้ง:
const groupBy = <T extends Record<K, PropertyKey>, K extends PropertyKey>(
objArr: readonly T[],
property: K,
) => objArr
.reduce((memo, x) => {
const value = x[property];
if (!memo[value]) {
memo[value] = [];
}
memo[value].push(x as any); // <-- here
return memo;
}, {} as Record<T[K], { [P in (PropertyKey & keyof T)]: WidenLiteral<T[P]> }[]>);
ที่นั่นจำเป็นต้องมีการยืนยันประเภทเนื่องจากแม้ว่า x จะชัดเจนว่าเป็นค่าของประเภท T แต่ก็ไม่ชัดเจนว่าคือค่าของประเภท {[P in ...]: WidenLiteral<...>} สำหรับคอมไพเลอร์อย่างน้อย คอมไพเลอร์ไม่ถนัดการให้เหตุผลลำดับสูงเกี่ยวกับประเภททั่วไปที่ยังไม่ได้ระบุเหมือน T ภายในร่างกายของ groupBy
ดังนั้นนี่จึงปลอดภัยต่อประเภทน้อยลงเล็กน้อยจากมุมมองของผู้ใช้งาน และคุณต้องมั่นใจว่าคุณเขียนมันถูกต้อง คอมไพเลอร์ไม่สามารถจับข้อผิดพลาดทั้งหมดด้วยการยืนยันประเภทได้จริง ๆ ดังนั้น push(x as any) และ push(123 as any) จึงดูเหมือนกันต่อคอมไพเลอร์
แต่จากมุมมองของผู้โทร ควรจะใช้งานได้ดี (ตราบใดที่ผู้โทรใช้งานชนิดเฉพาะเจาะจงและไม่ใช่ชนิดทั่วไปที่ไม่ได้ระบุ)
ลิงก์ Playground ไปยังโค้ด
