TypeScript doesn't automatically treat properties that accept undefined to be optional (although the converse, treating optional properties as accepting undefined, is true, unless you've enabled --exactOptionalPropertyTypes). There is a longstanding open feature request for this at microsoft/TypeScript#12400 (the title is about optional function parameters, not object properties, but the issue seems to have expanded to include object properties also). Nothing has been implemented there, although the discussion describes various workarounds.
Let's define our own workaround; a utility type UndefinedIsOptional<T> that produces a version of T such that any property accepting undefined is optional. It could look like this:
type UndefinedIsOptional<T extends object> = (Partial<T> &
{ [K in keyof T as undefined extends T[K] ? never : K]: T[K] }
) extends infer U ? { [K in keyof U]: U[K] } : never
That's a combination of Partial<T> which turns all properties optional, and a key remapped type that suppresses all undefined-accepting properties. The intersection of those is essentially what you want (an intersection of an optional prop and a required prop is a required prop) but I use a technique described at How can I see the full expanded contract of a Typescript type? to display the type in a more palatable manner.
Then we can define your type as
type IAPIRequest<B, P, Q> = UndefinedIsOptional<{
body: B;
params: P;
query: Q;
}>
and note that this must be a type alias and not an interface because the compiler needs to know exactly which properties will appear (and apparently their optional-ness) to be an interface. This won't matter much with your example code but you should be aware of it.
Let's test it out:
type ILR = IAPIRequest<{ email: string; password: string; }, undefined, undefined>
/* type ILR = {
body: {
email: string;
password: string;
};
params?: undefined;
query?: undefined;
} */
That looks like what you wanted, so you can define your ILoginRequest interface:
interface ILoginRequest extends IAPIRequest<
{ email: string; password: string; }, undefined, undefined> {
}
Also, let's just look at what happens when the property includes undefined but is not only undefined:
type Other = IAPIRequest<{ a: string } | undefined, number | undefined, { b: number }>;
/* type Other = {
body?: {
a: string;
} | undefined;
params?: number | undefined;
query: {
b: number;
};
} */
Here body and params are optional because undefined is possible, but query is not because undefined is impossible.
Playground link to code