-
Notifications
You must be signed in to change notification settings - Fork 21
Description
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
- 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- 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;
};
}- 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
- Read the contribution guide.
- Check existing discussions and issues.