openapi-typescript icon indicating copy to clipboard operation
openapi-typescript copied to clipboard

params always required

Open RPGillespie6 opened this issue 1 year ago • 1 comments

The following snippet of code should be valid, as per the README:

import createClient from "openapi-fetch";
import type { paths } from "./petstore"; // npx openapi-typescript https://petstore3.swagger.io/api/v3/openapi.yaml -o petstore.d.ts

const client = createClient<paths>();

client.POST("/store/order", {
    body: {
        id: 0,
    },
})

However, it is failing with:

error TS2345: Argument of type '{ body: {}; }' is not assignable to parameter of type '{ params: { query?: never; header?: never; path?: never; cookie?: never; }; } & { body?: { id?: number; petId?: number; quantity?: number; shipDate?: string; status?: "placed" | "approved" | "delivered"; complete?: boolean; }; } & { ...; } & Omit<...> & { ...; }'.
  Property 'params' is missing in type '{ body: {}; }' but required in type '{ params: { query?: never; header?: never; path?: never; cookie?: never; }; }'.

  6 client.POST("/store/order", {
                                ~
  7     body: {
    ~~~~~~~~~~~
...
  9     },
    ~~~~~~
 10 })
  ~

  node_modules/openapi-fetch/dist/index.d.ts:87:9
    87     : { params: T["parameters"] }
               ~~~~~~
    'params' is declared here.

If I change it to:

import createClient from "openapi-fetch";
import type { paths } from "./petstore"; // npx openapi-typescript https://petstore3.swagger.io/api/v3/openapi.yaml -o petstore.d.ts

const client = createClient<paths>();

client.POST("/store/order", {
    params: {},
    body: {
        id: 0,
    },
})

the error goes away.

Is this a recent regression? I don't remember it doing this before. I'm using typescript 5.5.4, openapi-fetch 0.10.2, and openapi-typscript 7.1.0.

my tsconfig.json:

{
    "compilerOptions": {
        "module": "ESNext",
        "moduleResolution": "node",
        "noImplicitAny": true,
    },
    "files": [
        "test.ts"
    ]
}

Checklist

RPGillespie6 avatar Jul 23 '24 16:07 RPGillespie6

I think the problem is here:

export type FindRequiredKeys<T, K extends keyof T> = K extends unknown ? (undefined extends T[K] ? never : K) : K;
/** Does this object contain required keys? */
export type HasRequiredKeys<T> = FindRequiredKeys<T, keyof T>;

A couple problems:

  1. K extends unknown seems to always be true (how can K extends unknown ever be false?)
  2. undefined extends T[K] seems to always be false (how can undefined extends T[K] ever be true?)

If I change it to this:

export type FindRequiredKeys<T, K extends keyof T> = never extends T[K] ? never : K;
/** Does this object contain required keys? */
export type HasRequiredKeys<T> = FindRequiredKeys<T, keyof T>;

It fixes it, but I'm not sure if that causes any regressions.

RPGillespie6 avatar Jul 23 '24 17:07 RPGillespie6

I tracked this down to behavioral differences based on the strictNullChecks (or strict, which includes strictNullChecks) compiler option. When that option is false (the default), undefined is considered to be a subtype of every other type except never. In that case, given this definition:

type Parameters = {
  query?: {
    name?: string;
    status?: string;
  };
  header?: never;
  path: {
    petId: number;
  };
  cookie?: never;
};

HasRequiredKeys<Parameters> will resolve to "header" | "cookie", which is clearly not the intent of that helper type. However, with strictNullChecks enabled, it correctly resolves to "path".

The problem is most noticeable in the case mentioned in the original issue, when no parameters are defined:

type Parameters = {
  query?: never;
  header?: never;
  path?: never;
  cookie?: never;
};

HasRequiredKeys<Parameters> resolves to "query" | "header" | "path" | "cookie", which then makes params a required property in the request init object.

I think we could fix this by replacing HasRequiredKeys with a helper type that doesn't rely on the inconsistent behavior of undefined extends T. I'm working on a PR that uses this helper instead:

type RequiredKeysOf<T> = {
  [K in keyof T]: {} extends Pick<T, K> ? never : K;
}[keyof T];

ngraef avatar Aug 08 '24 16:08 ngraef