javascript - ใช้วัตถุที่ซ้อนกันของฟังก์ชันใน TypeScript อย่างไร?
ฉันกำลังใช้ Redux และได้แยกข้อมูลในสโตร์ออกเป็น "สไลซ์" แต่ละ "สไลซ์" จะสอดคล้องกับส่วนต่าง ๆ เช่น users หรือ comments ฉันรวมตัวเลือกสำหรับแต่ละสไลซ์เหล่านี้เข้าด้วยกันเป็นโมดูล selectors ระดับบนสุดหนึ่งโมดูล ซึ่งฉันเข้าถึงตลอดทั้งแอปพลิเคชัน
ชิ้นหนึ่งมีรูปแบบดังนี้:
export interface IUsersSelectors {
getCurrentUser(state: IUsersState): IUser | undefined;
}
const selectors: IUsersSelectors = {
getCurrentUser(state: IUsersState) {
return state.currentUser;
}
};
export default {
getInitialState,
reducer,
selectors
};
ซึ่งทั้งหมดนั้นถูกนำเข้า แล้วตัวเลือกจะถูกรวมเข้าด้วยกัน:
export const selectors = Object.keys(slices).reduce((combinedSelectors: any, sliceKey: string) => {
const sliceSelectors = slices[sliceKey].selectors;
combinedSelectors[sliceKey] = Object.keys(sliceSelectors).reduce((selectorsMap: object, selectorKey: string) => {
const localizedSelector = sliceSelectors[selectorKey];
selectorsMap[selectorKey] = (globalState, ...args: any[]): any => {
return localizedSelector(globalState[sliceKey], ...args);
};
return selectorsMap;
}, {});
return combinedSelectors;
}, {});
และจากนั้นใช้ทั่วทั้งแอป:
selectors.users.getCurrentUser(store.getState());
สิ่งนี้หมายความว่า selectors คาดหวัง เพียงสถานะส่วนตัดของพวกเขาเมื่อดึงข้อมูล แต่จริงๆ แล้วพวกเขา ถูกเรียกใช้ ด้วยสถานะของ store ทั้งหมด โดยพื้นฐานแล้วฉันเพียงแค่ห่อพวกเขาในฟังก์ชันอีกตัวหนึ่งที่จัดการการกำหนดขอบเขต
สิ่งที่ใกล้เคียงที่สุดที่ฉันเคยทำในการกำหนดประเภททั่วไปสำหรับเรื่องนี้คือ:
type IScopedSelector<T extends () => any> = (globalState: IStoreState, ...args: any[]) => ReturnType<T>;
type IScopedSelectors<T> = {
[K in keyof T]: IScopedSelector<T[K]>;
};
type INestedScopedSelectors<R> = {
[S in keyof R]: IScopedSelectors<R[S]>;
};
export const selectors: INestedScopedSelectors<ISelectors>...
โดยที่ ISelectors เป็นอินเทอร์เฟซเรียบง่ายในรูปแบบดังนี้:
export interface ISelectors {
users: IUsersSelectors;
}
อย่างไรก็ตาม ฉันได้รับข้อผิดพลาดกับการพิมพ์นี้เมื่อพยายามส่ง T[K] ไปยัง IScopedSelector เนื่องจากมันต้องเป็นฟังก์ชัน:
[ts] Type 'T[K]' does not satisfy the constraint '() => any'.
ถ้าฉันเอา extends() => any ออกไป ฉันก็จะได้รับข้อผิดพลาดเกี่ยวกับ ReturnType:
[ts] Type 'T' does not satisfy the constraint '(...args: any[]) => any'.
ในอุดมคติ ฉันยังต้องการรักษาการกำหนดประเภทของพารามิเตอร์ตัวเลือกด้วยเช่นกัน (แทนที่จะเป็น ...args: any[]) โดยเพียงแค่แทนที่อาร์กิวเมนต์แรกให้เป็นสถานะของสโตร์ระดับโลก
มีวิธีที่ดีกว่าในการจัดการกับเจเนอริกซ้อนแบบนี้ไหม? สิ่งนี้เป็นไปได้หรือไม่?
คำตอบ #1
คุณจำเป็นต้องเพิ่มข้อจำกัดประเภทที่คล้ายกันไปยังประเภททั่วไปสำหรับทั้ง IScopedSelectors และ INestedScopedSelectors
หากไม่มีข้อจำกัดดังกล่าว คุณได้บอก TypeScript ว่า T ใน IScopedSelectors<T> สามารถเป็นชนิดใดก็ได้ ดังนั้น IScopedSelectors<string> ควรถูกต้อง แต่ TypeScript ชี้ให้เห็นอย่างถูกต้องว่าสำหรับชนิดข้อมูลหลายชนิด (เช่น string) [K in keyof T]: IScopedSelector<T[K]> จะไม่ทำงาน เนื่องจากไม่มีการรับประกันว่า T[K] จะเป็นไปตามข้อจำกัดที่กำหนดโดย IScopedSelector
ดังนั้นวิธีแก้ปัญหาคือเพียงแค่เพิ่มข้อจำกัดให้ทั้งสองอินเทอร์เฟซเพื่อให้ TypeScript มีการรับประกันนั้น สำหรับเรื่องนั้น ประเภท Record ที่มีอยู่แล้วอาจเป็นประโยชน์ ดังนั้นบางอย่างเช่น:
type IScopedSelectors<T extends Record<string, () => any>> = {
[K in keyof T]: IScopedSelector<T[K]>; // T[K] is now guaranteed to adhere to () => any
};
type INestedScopedSelectors<R extends Record<string, Record<string, () => any>>> = {
[S in keyof R]: IScopedSelectors<R[S]>; // Similarly, R[S] is now guaranteed to adhere to Record<string, () => any>, exactly what IScopedSelectors is expecting.
};
คุณอาจต้องการแทนที่ประเภท Record ด้วยบางสิ่งที่เฉพาะเจาะจงมากขึ้นขึ้นอยู่กับกรณีการใช้งานที่แน่นอนของคุณ แต่แนวทางแก้ปัญหาก็เหมือนเดิมโดยหลักการ แค่ตรวจสอบให้แน่ใจว่าได้ส่งต่อข้อจำกัดของคุณขึ้นไปตลอดลำดับขั้น
หวังว่านี่จะช่วยได้นะ!
