Skip to content

Type ambiguity with pathParamsAsTypes for overlapping dynamic paths #106

@simplg

Description

@simplg

Problem

When using the openapi-typescript option pathParamsAsTypes: true within the nuxt-open-fetch module, a type ambiguity arises for API endpoints that have overlapping dynamic paths.

This issue occurs because TypeScript's type system cannot distinguish between two paths when one is a dynamic substring of the other. For example, consider the following two endpoints:

  • /api/v1/user/{uuid}
  • /api/v1/user/{uuid}/edit

The openapi-typescript generator correctly translates these into the following path types:

  • path: `/api/v1/user/${string}`
  • path: `/api/v1/user/${string}/edit`

From a TypeScript perspective, "/edit" is just a string, which makes the second path type a valid sub-type of the first one. Consequently, when calling the fetch hook, TypeScript infers the wrong path type, leading to a type error when attempting to pass the second path.

This creates a usability problem where developers cannot leverage the full type safety benefits of the pathParamsAsTypes option for such common API structures.

Reproduction

  1. Sample OpenAPI schema (openapi.yaml)
openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /api/v1/user/{uuid}:
    get:
      summary: Get user by UUID
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  uuid:
                    type: string
                  login:
                    type: string
        default:
          description: Default error response
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: integer
                  message:
                    type: string
  /api/v1/user/{uuid}/edit:
    put:
      summary: Edit user by UUID
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                uuid:
                  type: string
                login:
                  type: string
                password:
                  type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties: {}
        default:
          description: Default error response
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: integer
                  message:
                    type: string
  1. Generated types (.nuxt/types/open-fetch/schemas/api.ts)
export interface paths {
  [path: `/api/v1/user/${string}`]: {
    get: {
      parameters: {
        query?: never;
        header?: never;
        path: {
          uuid: string;
        };
        cookie?: never;
      };
      requestBody?: never;
      responses: {
        200: {
          headers: {
            [name: string]: unknown;
          };
          content: {
            "application/json": {
              uuid?: string;
              login?: string;
            };
          };
        };
        /** @description Default error response */
        default: {
          headers: {
            [name: string]: unknown;
          };
          content: {
            "application/json": {
              code?: number;
              message?: string;
            };
          };
        };
      };
    };
    put?: never;
    post?: never;
    delete?: never;
    options?: never;
    head?: never;
    patch?: never;
    trace?: never;
  };
  [path: `/api/v1/user/${string}/edit`]: {
    get?: never;
    put: {
      parameters: {
        query?: never;
        header?: never;
        path: {
          uuid: string;
        };
        cookie?: never;
      };
      requestBody: {
        content: {
          "application/json": {
            uuid?: string;
            login?: string;
            password?: string;
          };
        };
      };
      responses: {
        /** @description OK */
        200: {
          headers: {
            [name: string]: unknown;
          };
          content: {
            "application/json": Record<string, never>;
          };
        };
        /** @description Default error response */
        default: {
          headers: {
            [name: string]: unknown;
          };
          content: {
            "application/json": {
              code?: number;
              message?: string;
            };
          };
        };
      };
    };
    post?: never;
    delete?: never;
    options?: never;
    head?: never;
    patch?: never;
    trace?: never;
  };
}
  1. Nuxt composable code (composable/useUser.ts)
import { useNuxtApp } from '#app';
export const useUser = (uuid: string) => {
const { $api } = useNuxtApp();

// This works fine:
const userData = await $api(`/api/v1/user/${uuid}`);

// This fails with a type error:
const updateData = await $api(`/api/v1/user/${uuid}/edit`, {
  method: "PUT",
  body: {
    login: "xxx",
    password: "xxx",
    uuid: "xxx"
  },
});
// TypeScript error: "Argument of type '{ login: string; password: string; uuid: string; }' is not assignable to parameter of type 'undefined'."
// The path type `"/api/v1/user/{uuid}"` is incorrectly matched.

return { userData,  updateData };
}

Workaround

A possible workaround is to define a type alias for string and manually cast the path parameters. This tricks TypeScript into treating the two paths as distinct types.

// .nuxt/types/open-fetch/schemas/api.ts (manual addition)
type APIString = string;

// Replace all ${string} with ${APIString} with .replace before writing the ts file.

Proposed solution

t would be highly beneficial to add a new option to the module's configuration that automates this workaround. This would simplify the developer experience and make the pathParamsAsTypes option more robust.

I propose a new configuration option, for example: pathParamTypeAlias.

openFetch: {
    openAPITS: {
      pathParamsAsTypes: true,
    },
    pathParamTypeAlias: 'APIString',
    clients: {
      api: {
        baseURL: "https://api.example.com",
        schema: "https://api.example.com/openapi.yaml",
      },
    },
  },

When this option is enabled, the module would internally replace all ${string} path types generated by openapi-typescript with the specified alias. This would allow developers to use the typed paths seamlessly without running into type ambiguity. The downside of this approach is that you need to cast every string in the path as the type alias specified (here "APIString").

So for example the above now working code would then be:

import { useNuxtApp } from '#app';
import type { APIString } from "#open-fetch";
export const useUser = (uuid: string) => {
const { $api } = useNuxtApp();

// This works fine:
const userData = await $api(`/api/v1/user/${uuid as APIString}`);

// This would now work
const updateData = await $api(`/api/v1/user/${uuid as APIString}/edit`, {
  method: "PUT",
  body: {
    login: "xxx",
    password: "xxx",
    uuid: "xxx"
  },
});

return { userData,  updateData };
}

Additional information

  • Would you be willing to help implement this feature?

Final checks

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions