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

Generic response type not working with useFetch #15401

Closed
szulcus opened this issue Nov 8, 2022 · 13 comments
Closed

Generic response type not working with useFetch #15401

szulcus opened this issue Nov 8, 2022 · 13 comments

Comments

@szulcus
Copy link

szulcus commented Nov 8, 2022

Versions

  • nuxt: 3.0.0-rc.12 & 3.0.0-rc.13
  • node: v16.11.0

Reproduction

https://stackblitz.com/edit/github-itsfns?file=app.vue

Works fine (asyncData is a _AsyncData<{ title: string }, true | FetchError<any> | null>:

const customApi = async () => {
	const asyncData = await useFetch<{ title: string }>('https://api.nuxtjs.dev/mountains', {
		pick: ['title'],
		method: 'GET',
	});
	return asyncData;
};
const result  = await customApi();
console.log(result.data.value?.title);

Not working (asyncData is a _AsyncData<PickFrom<_ResT, KeyOfRes<Transform>>, true | FetchError<any> | null>):

const customApi = async <T>() => {
	const asyncData = await useFetch<T>('https://api.nuxtjs.dev/mountains', {
		pick: ['title'],
		method: 'GET',
	});
	return asyncData;
};
const result  = await customApi<{ title: string }>();
console.log(result.data.value?.title);

Additionally, setting the `pick1 field causes an error.

Additional Details
I would like to add that everything worked very well before. Unfortunately, now even undoing the Nuxt version doesn't help.

Steps to reproduce

Copy and paste code above.

What is Expected?

I want to create a custom useFetch and be able to type a response (link1, link2)

What is actually happening?

I am getting a weird type causing my application to fail.

@szulcus
Copy link
Author

szulcus commented Nov 8, 2022

Temporary workaround:

const customApi = async <T>() => {
	const asyncData = await useFetch<T>('https://api.nuxtjs.dev/mountains', {
		// @ts-expect-error: https://github.com/nuxt/nuxt.js/issues/15401
		pick: ['title'],
		method: 'GET',
	}) as any; // eslint-disable-line @typescript-eslint/no-explicit-any
	return asyncData;
};
const result  = await customApi<{ title: string }>();
console.log(result.data.value?.title);

Copy link
Member

danielroe commented Nov 8, 2022

I'm afraid this is correct TS behaviour and not a bug. You need to specify that T must have a title field in order for this to work:

const customApi = async <T extends { title: any }>() => {
  const asyncData = await useFetch<T>('https://api.nuxtjs.dev/mountains', {
    pick: ['title'],
    method: 'GET'
  })
  return asyncData
}
const result = await customApi<{ title: string }>()
console.log(result.data.value?.title)

You can set typescript.strict to false to revert to the previous behaviour.

@danielroe danielroe transferred this issue from nuxt/nuxt Nov 8, 2022
@danielroe danielroe closed this as not planned Won't fix, can't repro, duplicate, stale Nov 8, 2022
@szulcus
Copy link
Author

szulcus commented Nov 8, 2022

@danielroe I set now 'strict' to 'false', reload editor and still have the same problem:
image
image

Copy link
Member

danielroe commented Nov 8, 2022

You're right; it seems it wasn't caused by enabling strict mode. The correct solution, from a TypeScript point of view, is the code I provided above.

@szulcus
Copy link
Author

szulcus commented Nov 8, 2022

I find it hard to believe because it is a bit contradictory to the idea of using generics. Especially since I was able to define every type successfully before. It turns out that from now on, every response from api must be any?

@danielroe
Copy link
Member

danielroe commented Nov 8, 2022

No; it's still fully typed. Your custom composable just needs to specify that any valid type must have a title field, because we're going to pick the title field. Note that the title field won't be typed as any - I'm just telling TS that any type for the field is valid.

@szulcus
Copy link
Author

szulcus commented Nov 8, 2022

The problem is that composable similar to customApi, where I defined baseURL, response parsing, error handling and a lot of other additions, I keep in a separate file. I wish I could use this anywhere in the app. In short, my code looks like this:

const customApi = async <T>(request: NitroFetchRequest, options: UseFetchOptions<T extends void ? unknown : T, (res: T extends void ? unknown : T) => T extends void ? unknown : T, KeyOfRes<(res: T extends void ? unknown : T) => T extends void ? unknown : T>>) => {
	const statusCode = ref<number>(200);
	const asyncData = await useFetch<T>(request, {
		initialCache: false,
		baseURL: <url>,
		headers: { ...headers, authorization: localStorage['auth-token'] }, // current localStorage
		onResponseError: async (ctx) => {
			statusCode.value = ctx.response.status;
			await onResponseError(ctx);
		},
		onRequest,
		parseResponse,
		...options,
	});
	return { ...asyncData, statusCode };
};

As you can see, I can't extends T here, because wherever I try to use this composable it will be a different type. Is there any other way for customApi to be fully typed?

@danielroe
Copy link
Member

In that case, you can copy the types from useFetch with their inference. It's a complex type but it needs to be for the inference:

https://github.com/nuxt/framework/blob/bdacfa6ffe65d8887e4830fa0d4c8ef2f424698c/packages/nuxt/src/app/composables/fetch.ts#L23-L33

The other alternative is to simply override them, e.g.:

const customApi = async <T>(request: NitroFetchRequest, options: UseFetchOptions<T extends void ? unknown : T, (res: T extends void ? unknown : T) => T extends void ? unknown : T, KeyOfRes<(res: T extends void ? unknown : T) => T extends void ? unknown : T>>) => {
	const statusCode = ref<number>(200);
	const asyncData = await useFetch(request, {
		initialCache: false,
		baseURL: <url>,
		headers: { ...headers, authorization: localStorage['auth-token'] }, // current localStorage
		onResponseError: async (ctx) => {
			statusCode.value = ctx.response.status;
			await onResponseError(ctx);
		},
		onRequest,
		parseResponse,
		...options,
	}) as AsyncData<T, FetchError | null>;
	return { ...asyncData, statusCode };
};

@szulcus
Copy link
Author

szulcus commented Nov 8, 2022

@danielroe At first glance, it seems to work. I don't know how to thank you, you are amazing! ❤️

Copy link
Member

You are very welcome 😊

@szulcus
Copy link
Author

szulcus commented Nov 8, 2022

@danielroe Sorry to bother you again, but I have encountered one irregularity. Nuxt RC13 decided to share the original error (nuxt/framework#8521) which I'm very happy about. Unfortunately, FetchError type doesn't have a few fields, including the status field, which I really care about. Is there a chance to easily replace this type with another, or do I have to create a new issue?
image
image

Copy link
Member

Yes, the error type is the second generic - AsyncData<T, MyCustomFetchError | null>;

@szulcus
Copy link
Author

szulcus commented Nov 9, 2022

Yes, I know. I mean a ready type with missing fields returned by useFetch (status, statusCode, statusMessage, statusText). These fields are not dependent on me. I hope this can be completed in the future. Temporarily I'll do it manually like this:

AsyncData<T, (FetchError<ApiError> & { status: number }) | null>

I'm not bothered anymore, thank you for your time and help.

@danielroe danielroe added the 3.x label Jan 19, 2023
@danielroe danielroe transferred this issue from nuxt/framework Jan 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants