typescript - แก้ไขวัตถุแบบเรียกซ้ำด้วยการเข้าถึงคุณสมบัติอย่างปลอดภัยตามประเภทในสองขั้นตอน
ฉันกำลังพยายามจะแทนที่ประเภทสตริงในฟังก์ชันต่อไปนี้ด้วยประเภทที่เฉพาะเจาะจงมากขึ้นซึ่งรับประกันการเข้าถึงคุณสมบัติที่ปลอดภัยตามประเภท:
import {get} from 'lodash';
const obj = {
foo: 'foo',
bar: {
a: 'Hello',
b: {c: 'World'}
}
};
function factory(namespace?: string) {
return function getter(key: string) {
return get(obj, [namespace, key].filter((part) => part != null).join('.'));
};
}
const getter = factory('bar');
getter('b.c'); // 'World'
สัญลักษณ์จุดระบุการเข้าถึงคุณสมบัติที่ซ้อนกัน สามารถปรากฏทั้งใน namespace และใน key
จนถึงตอนนี้ฉันได้พบว่าฉันสามารถพิมพ์ namespace ด้วยยูทิลิตี้นี้:
type NestedKeyOf<ObjectType extends object> =
{
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
: `${Key}`
}[keyof ObjectType & (string | number)];
การใช้งาน: namespace?: NestedKeyOf<typeof obj>.
อย่างไรก็ตาม ฉันกำลังประสบปัญหาในการคิดชนิดไดนามิกที่ถูกกำหนดค่าโดยอัตโนมัติให้กับ key
ข้อกำหนดเพิ่มเติมอีกสองข้อคือ:
- เนมสเปซควรแก้ไขไปยังออบเจ็กต์เท่านั้น (ไม่ใช่สตริงใบไม้)
- ตัวดึงค่าควรจะต้องให้ค่าเป็นสตริงใบสุดท้ายเสมอ (ไม่ใช่วัตถุ)
สามารถสันนิษฐานได้ว่ามีเพียงวัตถุและสตริงเท่านั้นที่อยู่ในวัตถุ ไม่มีสิ่งอื่นใด
// Test cases
// Should pass
factory()('foo')
factory('bar')('a')
factory('bar')('b.c')
// Invalid property access
// @ts-expect-error
factory('baz')
// @ts-expect-error
factory('bar')('d')
// Only partial namespaces are allowed
// @ts-expect-error
factory('foo')
// Getter calls need to resolve to a leaf string
// @ts-expect-error
factory('bar')('b')
การช่วยเหลือใด ๆ จะได้รับการชื่นชมอย่างมาก! ขอบคุณมาก!
คำตอบ #1
ฉันไม่อยากจะเชื่อเลยว่าสิ่งน่าขยะแขยงนี้จะใช้ได้จริง:
function factory<NestedKey extends NestedKeyOf<typeof obj>>(namespace?: NestedKey) {
return function getter<
TargetKey extends
(NestedKey extends undefined
? NestedKeyOf<typeof obj>
: NestedKeyOf<Get<typeof obj, NestedKey>>)
>(key: TargetKey): NestedKey extends undefined ? Get<typeof obj, TargetKey> : Get<typeof obj, `${NestedKey}.${TargetKey}`> {
return get(obj, [namespace, key].filter((part) => part != null).join('.'));
};
}
ขอฉันอธิบายก่อน อันดับแรก ฉันเจอปัญหาเยอะมากกับการใช้งาน NestedKeyOf ของคุณ :(
ฉันจำเป็นต้องเขียนใหม่เพราะระบบรายงานว่าการสร้างอินสแตนซ์ของประเภทลึกเกินไป ดังนั้นนี่คือเวอร์ชันของฉัน:
type NestedKeyOf<O> = O extends object ? {
[K in keyof O]: `${K & string}` | `${K & string}.${NestedKeyOf<O[K]>}`;
}[keyof O] : never;
มันทำสิ่งเดียวกัน; แค่เขียนต่างออกไป ต่อไปเราจำเป็นต้องสามารถ "ดึงค่าลึก" ของคุณสมบัติได้ (เลียนแบบฟังก์ชัน get ของ lodash):
type Get<O, P extends string> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof O ? Get<O[Key], Rest> : never
: P extends keyof O ? O[P] : never;
ส่วน extends keyof O เพิ่มเข้ามาเพื่อป้องกันข้อผิดพลาดเมื่อเข้าถึงคุณสมบัติที่ไม่ถูกต้อง
และสุดท้ายคือสิ่งประหลาดที่ฉันแสดงด้านบน
เราจำเป็นต้องเก็บสิ่งที่ namespace คือ ดังนั้นเราจึงใช้ generic ด้วย generic ตัวนี้ที่ตั้งชื่ออย่างเหมาะสมว่า NestedKey ตอนนี้เราสามารถใช้มันในนิยามของ getter ได้แล้ว
getter ยังรับคีย์แบบซ้อน อย่างไรก็ตามประเภทของมันจะแตกต่างกันเมื่อไม่ระบุ namespace
นั่นคือเหตุผลที่ NestedKey extends undefined อยู่ที่นั่น หากมันไม่ปรากฏขึ้น key ควรเป็นคีย์ซ้อนของอ็อบเจ็กต์เดิม มิฉะนั้น มันจะเป็นคีย์ซ้อนของค่าที่ namespace ชี้ไป โดยใช้ Get
สุดท้ายในค่าที่ส่งกลับ เราทำสิ่งเดียวกัน หาก NestedKey ไม่ปรากฏ เราก็จะดึงคีย์เป้าหมายอย่างลึก มิฉะนั้น เราจะดึงคีย์ซ้อนและคีย์เป้าหมายอย่างลึก
สนามเด็กเล่น
การยืนยัน as const คือเพื่อยืนยันว่ามันกำลังดึงค่าที่ถูกต้องอย่างแท้จริง
ปรับปรุงตามข้อกำหนดใหม่ เราจะต้องการประเภทใหม่บางอย่างเพื่อบอกเราว่าคีย์ใดเป็นวัตถุและคีย์ใดเป็นสตริง:
type GetObjectKeys<O, K extends string> = {
[P in K]: Get<O, P> extends string ? never : P;
}[K];
type GetStringKeys<O, K extends string> = {
[P in K]: Get<O, P> extends string ? P : never;
}[K];
พวกเขารับชนิดของวัตถุ และบางคีย์ที่เป็นไปได้ มันจะตรวจสอบชนิดของแต่ละคีย์ ในกรณีของวัตถุ หากมันเป็นสตริง มันจะเป็น never มิฉะนั้นเป็น P เราใช้ never เพราะด้านล่างเราจะได้การรวมของคีย์ที่เหลือทั้งหมดกับ [K] และ T | never จะกระชับเป็น T
แล้วเพราะว่า namespace เป็นทางเลือก จึงทำให้เกิดความยากลำบากบางอย่าง ความผิดพลาดจากฝั่งผมคือการเชื่อว่าถ้าไม่ได้ระบุ namespace NestedKey จะไม่ถูกกำหนด (ดังนั้นคำตอบก่อนหน้าจริงๆ แล้วไม่ถูกต้อง) สิ่งนี้ถูกแก้ไขในภายหลัง
ในการจัดการกับ namespace ที่เป็นตัวเลือก เราทำให้พารามิเตอร์ namespace ของฟังก์ชัน factory ดั้งเดิม ไม่ใช่ ตัวเลือก และเปลี่ยนชื่อเป็น _factory (ภายใน/ส่วนตัว) จากนั้นเราสร้างฟังก์ชัน factory ใหม่ที่เรียกใช้มันดังนี้:
function factory<NestedKey extends GetObjectKeys<typeof obj, NestedKeyOf<typeof obj>>>(namespace?: NestedKey) {
return _factory<
{ __private: typeof obj },
GetObjectKeys<typeof obj, NestedKeyOf<typeof obj>> extends NestedKey ? "__private" : `__private.${NestedKey}`
//@ts-ignore Unfortunately I don't think there is a good way to prevent this error
>({ __private: obj }, namespace ? `__private.${namespace}` : "__private");
}
มันคาดหวังคีย์ของวัตถุใด ๆ แต่ถ้า namespace ไม่ถูกระบุ มันจะสร้างค่าเริ่มต้นเป็น __private เพราะเราห่อวัตถุเป้าหมายในวัตถุอื่นที่มีคุณสมบัติ __private เพื่อหลีกเลี่ยงข้อเท็จจริงที่ว่า namespace เป็นค่าเลือก คิดเรื่องนี้เหมือนกับตัวแทนมอบหมาย
ตอนนี้สำหรับฟังก์ชัน _factory ที่ปรับแก้แล้ว:
function _factory<Obj extends unknown, NestedKey extends NestedKeyOf<Obj>>(obj: Obj, namespace: NestedKey) {
return function getter<
TargetKey extends GetStringKeys<Get<Obj, NestedKey>, NestedKeyOf<Get<Obj, NestedKey>>>
>(key: TargetKey): Get<Obj, `${NestedKey}.${TargetKey}`> {
return get(obj, [namespace, key].filter((part) => part != null).join('.'));
};
}
ฟังก์ชันนี้โดยส่วนใหญ่เหมือนกับต้นฉบับ แต่ตอนนี้มันคาดหวังเฉพาะกุญแจที่ให้ผลลัพธ์เป็นสตริง ส่วนที่จัดการกับ namespace ที่เป็นทางเลือกถูกย้ายไปยังฟังก์ชัน factory ใหม่
ฉันน่าจะเลือกชื่อที่ดีกว่าสำหรับ factory และ _factory เพื่อหลีกเลี่ยงความสับสน แต่หวังว่าคุณจะตามฉันมาได้พอสมควร
สนามเด็กเล่น
เอ่อ นี่มันซับซ้อนเกินไปจริง ๆ; มันมักจะเป็นแบบนี้เสมอเมื่อคุณเริ่มทำให้ประเภททำงานเหมือนโค้ดจริง 😛
