inertiajs / inertia

Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers.
https://inertiajs.com
MIT License
6.23k stars 420 forks source link

usePage types not inheriting @inertiajs/core Page type #1381

Closed nurdism closed 1 year ago

nurdism commented 1 year ago

Version:

Describe the problem:

@inertiajs/vue3/types/app.d.ts@usePage types are declared as

export declare function usePage(): {
    component?: string;
    props?: {
        [x: string]: unknown;
        errors: import("@inertiajs/core").Errors & import("@inertiajs/core").ErrorBag;
    };
    url?: string;
    version?: string;
    scrollRegions?: {
        top: number;
        left: number;
    }[];
    rememberedState?: Record<string, unknown>;
    resolvedErrors?: import("@inertiajs/core").Errors;
};

it does not inherit from the Page type from @inertiajs/core and it causing all instances of usePage() to ignore the PageProps interface in @inertiajs/core/types/types.ts

if extending the PageProps interface via

declare module '@inertiajs/core' {
  interface PageProps {
    myProp: string
  }
}

it does not reflect in usePage() typings

travis-r6s commented 1 year ago

I managed to override this by re-declaring + re-exporting the whole function, as it isn't an interface:

import { router } from '@inertiajs/vue3'

declare module '@inertiajs/vue3' {
  export declare const router: router

  export declare function usePage<Props> (): {
    component: string;
    props: Props & {
      errors?: import('@inertiajs/core').Errors & import('@inertiajs/core').ErrorBag;
    };
    url: string;
    version?: string;
    scrollRegions?: {
      top: number;
      left: number;
    }[];
    rememberedState?: Record<string, unknown>;
    resolvedErrors?: import('@inertiajs/core').Errors;
  }
}

Just out of interest, was there a particular reason the generic type argument was removed from the usePage function? I used to be able to type the props with usePage<SomePropsInterface>, but now I need to use page.props as unknown as SomePropsInterface, which doesn't look particularly neat...

Similarly, the useForm extends Record<string, unknown>, which causes some type errors if I have an interface with optional keys for example... I managed to fix this by re-exporting those functions too:

function useForm<TForm = Record<string, unknown>> (data: TForm): InertiaForm<TForm>
export default function useForm<TForm = Record<string, unknown>> (rememberKey: string, data: TForm): InertiaForm<TForm>
nurdism commented 1 year ago

I have also come up with a solution to this by re-exporting, like so: globals.d.ts

import type { Page } from '@inertiajs/core'
declare module '@inertiajs/vue3' {
  export declare function usePage<T>(): Page<T>
}

the Page type accepts a generic

travis-r6s commented 1 year ago

Ah, perfect, that's a much nicer solution! Thanks @nurdism

WalrusSoup commented 1 year ago

export declare function usePage(): Page

I'm not sure why, but this was throwing an error for me. I adjusted it and and it seems to be ok with this instead?

import type { Page } from '@inertiajs/core';
declare module '@inertiajs/vue3' {
    export function usePage<T>(): Page<T>
}
johan-paqt commented 1 year ago

export declare function usePage(): Page

I'm not sure why, but this was throwing an error for me. I adjusted it and and it seems to be ok with this instead?

import type { Page } from '@inertiajs/core';
declare module '@inertiajs/vue3' {
    export function usePage<T>(): Page<T>
}

This worked like a charm for me, and I combine it with my own definition for the props, like so:

import type { Page, PageProps } from '@inertiajs/core';

declare module '@inertiajs/vue3' {
    export function usePage<T>(): Page<T>;
}

declare global {
    export interface InertiaProps extends Page<PageProps> {
        flashNotifications: Array<App.FlashNotification>;
    }
}

And then you can use it like so:

const notifications = computed(() => usePage<InertiaProps>().props.flashNotifications);

And get full autocompletion for props.

I'm not a typescript expert, maybe there is a better way of doing this, please let me know, but it works like a charm.

nurdism commented 1 year ago

declare module '@inertiajs/core' {
  interface PageProps { // you can define global $page/usePage() props by extending/defining this interface 
    myGlobalProp: string[]
  }
}

declare module '@inertiajs/vue3' {
  export function usePage<T>(): Page<T> // the T generic will combine any type you add to it & the PageProps interface defined in @inertiajs/core
}

const page = usePage<{ myLocalProp: string }>() // the types for page.props should be { myGlobalProp: string[]; myLocalProp: string }
const page = usePage() // the types for page.props should be { myGlobalProp: string[]; }
johan-paqt commented 1 year ago

declare module '@inertiajs/vue3' {
  export function usePage<T>(): Page<T> // the T generic will combine any type you add to it & the PageProps interface defined in @inertiajs/core
}

You just blew my mind, thank you! Much, much cleaner.

WalrusSoup commented 1 year ago

This worked like a charm for me, and I combine it with my own definition for the props, like so:

I use it slightly differently than your example. Seems to work fine:

import type { Page } from '@inertiajs/core';

declare module '@inertiajs/vue3' {
    export function usePage<T>(): Page<T>
}

interface GlobalProps extends Page {
    meta: {
        user: LoggedInUser
    },
    flash: {
        message: string | null,
        error: string | null
    }
}

interface ManageUserPageProps extends GlobalProps {
    user: FullUser
    available_roles: Role[]
    available_permissions:  string[]
}

I shove all my page prop definitions into that file. Then, in my vue files:

const available_roles = usePage<ManageUserPageProps>().props.available_roles;

Seems to work fine.

jessarcher commented 1 year ago

Just out of interest, was there a particular reason the generic type argument was removed from the usePage function? I used to be able to type the props with usePage<SomePropsInterface>, but now I need to use page.props as unknown as SomePropsInterface, which doesn't look particularly neat...

I forgot to copy that definition from the old type definitions when converting the adapters to TypeScript, so they were just inferred without the generic.

I've created https://github.com/inertiajs/inertia/pull/1394 that attempts to address this; however, I can't currently get it to build.

feras1984 commented 10 months ago
import type { Page } from '@inertiajs/core';
declare module '@inertiajs/vue3' {
export function usePage<T extends PageProps>() : Page<T>
}

While T accepts a generic type, Page accepts PageProps interface. So, we need to add T extends PageProps


Smef commented 7 months ago

I've been trying to work with typing usePage() using PNPM. Has anyone had any luck with that? With PNPM you can't import types from @inertiajs/core since it's a child dependency of @inertiajs/vue3 and not a direct project-level dependency.

Separately installing @inertiajs/core as a project dependency would be possible, but it seems like a bad idea as you'd need to separately manage the versions of these two packages instead of just letting @inertiajs/vue3 keep track of the versions on its own.

Is there a way to type usePage() without also adding @inertiajs/core as a project-level dependency?

Here's what works great if you can import from @inertiajs/core (which works fine with NPM). This updated the definition of usePage everywhere so you don't have to manually type it each time:

SharedProps.d.ts

import type User from "@/Types/Models/User";
import type { Page, PageProps } from "@inertiajs/core";

interface SharedProps extends PageProps {
    auth: {
        user: User;
    };
}

export default SharedProps;

declare module "@inertiajs/vue3" {
    export function usePage(): Page<SharedProps>;
}

The solution below sort of works with PNPM, but it less convenient because you have to manually type every instance of usePage. The [key: string]: any bit also seems like it isn't a good idea, but seems to be necessary:

SharedProps.d.ts

import type User from "@/Types/Models/User";

interface SharedProps{
    auth: {
        user: User;
    };
    [key: string]: any;
}

export default SharedProps;
const page = usePage<SharedProps>();
rikarani commented 5 months ago
import type User from "@/Types/Models/User";
import type { Page, PageProps } from "@inertiajs/core"; 
interface SharedProps extends PageProps {
     auth: {
         user: User;
     };
 }

export default SharedProps;

declare module "@inertiajs/vue3" {
     export function usePage(): Page<SharedProps>;
}

thanks @Smef, it works on me

melvinotieno commented 4 months ago

I've been trying to work with typing usePage() using PNPM. Has anyone had any luck with that? With PNPM you can't import types from @inertiajs/core since it's a child dependency of @inertiajs/vue3 and not a direct project-level dependency.

Separately installing @inertiajs/core as a project dependency would be possible, but it seems like a bad idea as you'd need to separately manage the versions of these two packages instead of just letting @inertiajs/vue3 keep track of the versions on its own.

Is there a way to type usePage() without also adding @inertiajs/core as a project-level dependency?

@Smef @rikarani

Solution for when working with pnpm that I figured out

  1. Create .npmrc
  2. Add the following line public-hoist-pattern[]=@inertiajs/core
  3. Do a clean install pnpm install

Reference https://pnpm.io/npmrc#public-hoist-pattern