Write a generic ApiResponse<T> type with success and error states
modeling exclusive states with discriminated unions and generics.
define two interfaces sharing a status literal, one with data: T and the other with error: { code; message; }.
WHAT THIS TESTS:
This question tests your ability to model mutually exclusive states in TypeScript using discriminated unions combined with generics. The interviewer wants to see that you understand how to make illegal states unrepresentable rather than relying on runtime checks or optional fields. Specifically they are looking for familiarity with literal types, union types, generic type parameters, and how TypeScript uses a discriminant property to narrow unions. They also care that you can explain why this is preferable to a single flat interface with optional fields.
A GOOD ANSWER COVERS:
A strong answer defines two separate interfaces or types that share a common discriminant property called status with a string literal type. The first interface should be generic and use status: "success" along with a data property of type T. The second interface should use status: "error" along with an error property that is an object containing code and message strings. Then you combine them into a union type such as type ApiResponse<T> = SuccessResponse<T> | ErrorResponse. A good candidate also explains that this pattern enables type narrowing, so checking response.status === "success" automatically narrows the type and makes data available while error becomes inaccessible, and vice versa. Mentioning that the error object itself should be typed and not just a primitive string shows attention to detail.
COMMON WRONG ANSWERS:
The most common mistake is creating a single interface with optional properties like data?: T and error?: { code: string; message: string } alongside a status string that is not a literal union. This approach fails to enforce exclusivity because TypeScript will allow an object where both fields are present, both are absent, or the status does not match the payload. Another red flag is using any for the error or data fields, which defeats the purpose of type safety. Some candidates also forget to make the success branch generic, hardcoding unknown or any instead of T. Others might use a boolean flag like isError instead of a string literal discriminant, which still works but is less explicit and harder to extend.
LIKELY FOLLOW-UPS:
The interviewer might ask how you would handle HTTP status codes alongside this type, or how to add a loading state to the union. They could ask you to write a type guard function or a helper that unwraps the data and throws if the response is an error. Another follow-up is how to make the error type generic as well, such as ApiResponse<T, E>, or how to enforce that fetch wrappers always return this shaped response. You might also be asked to derive a utility type that extracts only the success payload or to handle pagination metadata that appears only on success.
ONE CONCRETE EXAMPLE:
Imagine a user profile endpoint. With the discriminated union, a successful response is typed as { status: "success", data: { id: number, name: string } } and an error response is typed as { status: "error", error: { code: 404, message: "User not found" } }. In consuming code you can write if (res.status === "error") { console.error(res.error.message); return; } and TypeScript knows that inside the else block res.data exists and is the user object. If you had used optional fields instead, res.data might still be undefined in the else block and you would lose compile time guarantees. This means downstream components can safely destructure the user without defensive checks.
Read the original → typescriptlang.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.