Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

$fetch type safety for query and body parameters #938

Open
1 task
ozum opened this issue Feb 10, 2023 · 3 comments
Open
1 task

$fetch type safety for query and body parameters #938

ozum opened this issue Feb 10, 2023 · 3 comments
Labels
enhancement New feature or request typescript

Comments

@ozum
Copy link

ozum commented Feb 10, 2023

Describe the feature

$fetch provides type safety for return types which is great. It would be greater if it optionally checks types for query and body parameters for internal API requests.

Below is a rough proposal:

  1. Server routes optionally export QuerySchema and BodySchema.
    -> Developer's responsibility
  2. Generate necessary types for those routes /.nuxt/types/nitro.d.ts.
    -> Below is an example.
  3. Add types to /node_modules/nitropack/dist/index.d.ts
    -> Below is a proposal.

/server/api/product.units.ts

export default defineEventHandler((event) => {
  const query = getQuery(event)
  const body = await readBody(event)
})

export interface QuerySchema {
  name: string;
  id: number;
}

export interface BodySchema {
  content: string
}

/.nuxt/types/nitro.d.ts

// It would be better if `InternalApi` and the proposed `InternalApiQuerySchema` and `InternalApiQuerySchema`
// are merged into one interface.
// However separated interfaces are easier to implement for re-using the current code base.

declare module 'nitropack' {
  interface InternalApi {
    '/api/units': {
      'get': Awaited<ReturnType<typeof import('../../server/api/units.get').default>>
    }
  }

  interface InternalApiQuerySchema {
    "/api/units": {
      get: import("../../server//api/units.get").QuerySchema;
    };
  }

  interface InternalApiBodySchema {
    "/api/units": {
      get: import("../../server//api/units.get").BodySchema;
    };
  }
}

/node_modules/nitropack/dist/index.d.ts

// ─── Added ───────────────────────────────────────────────────────────────────
type RequestSchema<Base, R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> = R extends keyof Base
  ? M extends keyof Base[R]
    ? Base[R][M]
    : never
  : never;

// ─── Modified ────────────────────────────────────────────────────────────────
// Added `query` and `body`
interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>>
  extends Omit<FetchOptions, "query" | "body"> {
  method?: Uppercase<M> | M;
  query?: RequestSchema<InternalApiQuerySchema, R, M>;
  body?: RequestSchema<InternalApiBodySchema, R, M>;
}

Problems I stumbled upon:

  1. Related to TS error Excessive stack depth comparing types when trying to wrap $fetch #470. I get Excessive stack depth comparing types... error from TypeScript. This error is present even I copy-paste the types without changing them. The problem is caused by AvailableRouterMethod<R> type. If I switch it with RouterMethod, it works. In this case we sacrifice "method" safety. TBH, I prefer query and post safety to the "method"
    safety.
    a. Query and body parameters are much more error prone compared to a simple method name.
    b. AvailableRouterMethod<R> type seems much more expensive compared to simple object types.
  2. I don't know how to generate types /.nuxt/types/nitro.d.ts. I guess it would be easy to utilize already existing type generation function.

POC

Below is the POC: A composable for Nuxt representing Excessive stack... problem mentioned above.

POC Code

/server/api/units.get.ts

import { useValidatedQuery, useValidatedBody, z } from "h3-zod";
import type { H3Event } from "h3";

const querySchema = z.object({ language: z.string() });
const bodySchema = z.object({ color: z.number() });

export type QuerySchema = z.infer<typeof querySchema>;
export type BodySchema = z.infer<typeof bodySchema>;

export default eventHandler(async (event: H3Event) => {
  const { language } = useValidatedQuery(event, querySchema);
  const { color } = useValidatedBody(event, bodySchema);
  return { color, language };
});

/composables/useSafeFetch.ts

import { NitroFetchRequest, TypedInternalResponse, ExtractedRouteMethod, AvailableRouterMethod } from "nitropack";
import { FetchOptions, FetchResponse } from "ofetch";
import type { InternalApiQuerySchema, InternalApiBodySchema } from "internal-api-schema";

// Types from `/node_modules/nitropack/dist/index.d.ts`

// ─── Added ───────────────────────────────────────────────────────────────────
type RequestSchema<Base, R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> = R extends keyof Base
  ? M extends keyof Base[R]
    ? Base[R][M]
    : never
  : never;

// ─── Modified ────────────────────────────────────────────────────────────────
// Added `query` and `body`
interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>>
  extends Omit<FetchOptions, "query" | "body"> {
  method?: Uppercase<M> | M;
  query?: RequestSchema<InternalApiQuerySchema, R, M>;
  body?: RequestSchema<InternalApiBodySchema, R, M>;
}

// ─── Not Changed ─────────────────────────────────────────────────────────────
interface $Fetch<DefaultT = unknown, DefaultR extends NitroFetchRequest = NitroFetchRequest> {
  <T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(
    request: R,
    opts?: O
  ): Promise<TypedInternalResponse<R, T, ExtractedRouteMethod<R, O>>>;
  raw<T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(
    request: R,
    opts?: O
  ): Promise<FetchResponse<TypedInternalResponse<R, T, ExtractedRouteMethod<R, O>>>>;
  create<T = DefaultT, R extends NitroFetchRequest = DefaultR>(defaults: FetchOptions): $Fetch<T, R>;
}

const useSafeFetch: $Fetch = (request, opts) => $fetch(request, opts);
useSafeFetch.raw = (request, opts) => $fetch.raw(request, opts);
useSafeFetch.create = (defaults) => $fetch.create(defaults);

export default useSafeFetch;

/.nuxt/types/nitro.d.ts

declare module "internal-api-schema" {
  interface InternalApiQuerySchema {
    "/api/units": {
      get: import("../../server/api/units.get").QuerySchema;
    };
  }

 interface InternalApiBodySchema {
    "/api/units": {
      get: import("../../server/api/units.get").BodySchema;
    };
  }
}

Additional information

  • Would you be willing to help implement this feature?
@TheAlexLichter TheAlexLichter added enhancement New feature or request and removed pending triage labels Feb 10, 2023
@septatrix
Copy link
Contributor

This would be fabulous if implemented to work with #1162 and could replace our usage of nestjs and potentially some FastAPI Python backends.

@septatrix
Copy link
Contributor

It would be more natural if those types where instead generic arguments to defineEventHandler

@pi0 pi0 added typescript and removed good first issue Good for newcomers labels May 16, 2024
@Bobakanoosh
Copy link

Would love this, I had ssumed that since h3's defineEventHandler takes:

interface EventHandlerRequest {
    body?: any;
    query?: QueryObject;
    routerParams?: Record<string, string>;
}

that I could pass a custom one to it's generic parameter and have it be e2e typed into $fetch, but sadly isn't the case :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request typescript
Projects
None yet
Development

No branches or pull requests

6 participants