2

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

T00rk
  • 2,178
  • 3
  • 17
  • 23
  • Does [this approach](https://www.typescriptlang.org/play?ts=5.0.4#code/C4TwDgpgBAkgzgOQgNwgJwDwBUB8UC8UA2lgLpQQAewEAdgCZzG0rrkD8UwaArtAFxQAZgEMANnAgBuALAAoeVTAB7NMC7hoMAIIAFGACUIARz5xgGAEIAaKLoJQWqNLYCKDp+jyEA3vKhQAEbK9CCClrJyAL5QAGRQABTwSM4YunhUNAxM3HxQnD4xgj5QYCJoIgC2cIL2UQCUcf6JyayYrhnUdIxcvNAFRVAlpuhhUO4NkYqUKmoakLAAMsoA5gCWtEYj5g46+ltmFiUQlSJrYoLmaBsrUqUicHAA7qr0l9w3UFE4U3IAxspaDsxKsNg4HiBaH9EiVgqEvoIYMt1psTIdGvg8IVfgCgeo0BA4CogdBCCCUQk-HIAnCxsdTudBAAiBliAACNHMADoAZUmbYyo8Xmg3lAmc4QFdAStBc9XkyvtZ5JN5EA) work for you? If yes, I'll write an answer explaining; If not, what am I missing? – wonderflame Jun 01 '23 at 16:44
  • @wonderflame I still have the same error with your approach. `Argument of type '{ body: { email: string; password: string; }; }' is not assignable to parameter of type 'ILoginRequest'.Property 'params' is missing in type '{ body: { email: string; password: string; }; }' but required in type '{ params: undefined; }` – T00rk Jun 01 '23 at 16:56
  • 1
    You might be looking for [this version of your code](https://tsplay.dev/wj897m) which turns all properties accepting `undefined` optional. Does that work for you? I could write up an answer if so. If not, what doesn't work about it? – jcalz Jun 01 '23 at 17:00
  • 1
    @T00rk can you include the code when you get the error? Because it's okay in the playground – wonderflame Jun 01 '23 at 17:01
  • If you want field to be empty you should pass `never`, not `undefined` – wonderflame Jun 01 '23 at 17:01

2 Answers2

1

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

jcalz
  • 264,269
  • 27
  • 359
  • 360
-1

You don't need to extend your generic types for that case, you can set default values instead.

export interface IAPIRequest<B , P = undefined, Q = undefined> {
    body: B;
    params?: P;
    query?: Q;
}

Then you don't have to provide the default when using it:

export interface ILoginRequest extends IAPIRequest<{ email: string; password: string; }> {}

Florian Walch
  • 84
  • 1
  • 3
  • This doesn't seem to be what the OP is asking; it's not about the *type arguments* being optional, but the *properties*; OP wants `IAPIRequest` to only require the `params` property. – jcalz Jun 01 '23 at 16:48
  • Here body is mandatory. I have requests where I don't provide a body. – T00rk Jun 01 '23 at 17:05