如何为返回多类型响应的TypeScript请求函数声明类型?
Great question! The issue you're hitting comes down to TypeScript not being able to narrow down which specific response type you're working with when using a union type like GenericResponse. Since the union includes multiple payload shapes, TypeScript can't guarantee that response.payload.project_id or response.payload.devices exist on every possible member of the union—hence the "implicit any" error.
Your initial instinct to create individual response types per request is correct! Instead of using a union, we can leverage generics and mapped types to tie specific commands to their corresponding response types, making your code fully type-safe without any any workarounds.
Step 1: Define Request-Specific Response Types
First, keep your per-request response types (and add any missing ones like OpenProjectResponse):
// Define response types for each command export type OpenProjectResponse = { payload: { project_id: string } }; export type CreateProjectResponse = { payload: { project_id: string } }; export type GetDevicesResponse = { payload: { devices: DeviceResponse[] } }; // Define the DeviceResponse type to avoid implicit any in getDevices export type DeviceResponse = { type: string; device_id: string; name: string; };
Step 2: Map Commands to Their Response Types
Create a mapped type that links each command string to its corresponding response. This lets TypeScript automatically infer the right response type based on the cmd you pass:
// Map each command to its expected response type type CommandToResponse = { open_project: OpenProjectResponse; create_project: CreateProjectResponse; get_devices: GetDevicesResponse; }; // Define a type for valid request bodies, tied to the command-response map type RequestBody<T extends keyof CommandToResponse> = { type: 'request'; cmd: T; payload?: Record<string, any>; // You can narrow this further per command if needed };
Step 3: Update the request Method with Generics
Modify your request method to use the mapped type, so it returns a promise of the exact response type matching the command:
request<T extends keyof CommandToResponse>(body: RequestBody<T>): Promise<CommandToResponse[T]> { const transactionId = this.newTransactionId(); const requestMessage = JSON.stringify({ ...body, trans_id: transactionId }); if (this.logger) this.logger(requestMessage); return new Promise<CommandToResponse[T]>((resolve, reject) => { const timeoutTimer = setTimeout(() => { const timeoutMessage = 'Request timed out'; if (this.logger) this.logger(timeoutMessage); reject(new Error(timeoutMessage)); }, this.maxTime); this.requestCallbacks[transactionId] = { resolver: resolve, rejecter: reject, timeoutTimer }; this.socket.send(requestMessage); }); }
Step 4: Update Your Method Calls
Now, when you call request, TypeScript will automatically infer the correct response type based on the cmd you provide—no more implicit any warnings!
async openProject(fileName: string, force: boolean, progress: boolean) { // TypeScript infers response as OpenProjectResponse automatically const response = await this.request({ type: 'request', cmd: 'open_project', payload: { value: fileName, force, progress } }); return { id: response.payload.project_id }; // Fully type-safe! } async createProject() { const response = await this.request({ type: 'request', cmd: 'create_project' }); return { id: response.payload.project_id }; } async getDevices() { const response = await this.request({ type: 'request', cmd: 'get_devices' }); return response.payload.devices .filter((device: DeviceResponse) => device.type === 'device') .map((device) => ({ id: device.device_id, name: device.name })); }
Why This Works Better Than a Union
- Full type safety: No more
anytypes or TypeScript warnings—every property access is validated at compile time. - Automatic inference: You don't have to manually specify the response type for each request; TypeScript handles it based on the
cmd. - Scalability: When you add a new command, just add its response type to
CommandToResponseand TypeScript will enforce the rest.
If you ever need to narrow down payload types for specific commands, you can extend the RequestBody type to have command-specific payload shapes (e.g., using another mapped type), but the above setup covers the core issue you're facing.
内容的提问来源于stack exchange,提问作者user1283776

