openapi-ts / openapi-typescript

Generate TypeScript types from OpenAPI 3 specs
https://openapi-ts.dev
MIT License
5.47k stars 450 forks source link

params always required #1778

Closed RPGillespie6 closed 3 weeks ago

RPGillespie6 commented 1 month ago

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 commented 1 month ago

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.

ngraef commented 4 weeks ago

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];