I try to define a custom interfaces like this :
export interface IAPIRequest<B extends any, P extends any, Q extends any>
{
body: B;
params: P;
query: Q;
}
This type is supposed to be extended in a lot of other types for each request mu API is supposed to handle.
For example :
export interface ILoginRequest extends IAPIRequest<{ email: string; password: string; }>, undefined, undefined> {}
It works a little but everytime I use this interface, I must provide all the properties even if they are undefined.
Example:
const login = async ({ body }: ILoginRequest) =>
{
...
}
const response = await login({ body: { email: 'mail@test.com', password: 'verystrongpassword' }, params: undefined, query: undefined });
It doesn't work if I don't provide the undefined properties.
How can I define an abstract type for IAPIRequest that would avoid me from providing undefined values ?
PS : I've tried this as well
export interface IAPIRequest<B extends any, P extends any, Q extends any>
{
body?: B;
params?: P;
query?: Q;
}
Even for IAPIRequest<B, P, Q> where none of B, P, or Q allow undefined, I still get that the properties might be undefined
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.