eidellev / inertiajs-adonisjs

280 stars 17 forks source link

feat(types): add InertiaPage type helper #110

Open codytooker opened 1 year ago

codytooker commented 1 year ago

Based off of my enhancement request here https://github.com/eidellev/inertiajs-adonisjs/issues/109

The Problem

When using Inertia with Typescript there is currently no type safety when it comes to the frontend framework page that is rendered.

You can do something like this but we are sort of just faking the type safety here because things can get out of sync

interface FooPageProps {
   bar: string
}

const FooPage = ({ bar }: FooPageProps) => {
    return <div>{bar}</div>
}

Simply by adding some generics to the Inertia.render function we can then create a helper type that can pull the ResponseProps out for us.

So now we can do the following

import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { InertiaPage } from '@ioc:EidelLev/Inertia'

export default class FooController {
  public async index({ inertia }: HttpContextContract) {

    return inertia.render('Foo', {
      bar: "FooBar",
    })
  }
}

export type FooIndexProps = InertiaPage<FooController['index']>
import type FooIndexProps from 'App/Controllers/FooController'

const FooPage = ({ bar }: FooIndexProps) => {
    return <div>{bar}</div>
}

FooIndexProps now equals

type FooIndexProps = {
    bar: string
}
eidellev commented 1 year ago

Great work. Can you please update the readme?

codytooker commented 1 year ago

Added a readme. Is the name of the Type good in your opinion? With this approach there is the need to make a new type and export it for every controller method. If this is a resource method, It could end up being annoying to do so. I think I can take it one step further and allow for something like

type FooControllerProps = InertiaController<FooController>

which would produce something like

type FooControllerProps = {
    index: {
         foos: string[]
    }
    show: {
          foo: string
     }
}
eidellev commented 1 year ago

🤔 I think I like the second approach better since it's less verbose. How would you use this in the client?

codytooker commented 1 year ago

On the client you would use it like so

import type FooControllerProps from 'App/Controllers/FooController'

const FooPage = ({ bar }: FooControllerProps['show']) => {
    return <div>{bar}</div>
}

The type to get this to work is

type InertiaController<T> = {
  [K in keyof T]: T[K] extends AdonisControllerMethod
    ? InertiaPage<T[K]>
    : never
}

This works, but after a little more testing, InertiaPage does not work if for instance there are multiple returns in a method, as it cannot figure out which return will happen. I can do a little more testing to see if I can come up with a fix for it.

As an example of what I am talking about. If we have the folllowing

export default class FooController {
  public async index({ inertia, request }: HttpContextContract) {
   if(request.params().noInertia) {
      return {
        test: 'test',
      }
    }
    return inertia.render('Foo', {
      bar: "FooBar",
    })
  }
}

InertiaPage<FooController['index']> // = unknown
eidellev commented 1 year ago

thanks. it's an interesting problem. what if we explicitly type the controller method intead?

codytooker commented 1 year ago

I think there is ways around it. I'll have more time to dig in over the next few days and see what kind of solution I can come up with.

mle-moni commented 10 months ago

Hello, I'm a bit late, but I usually use this for type safety:

// app/utils/inertiaRender.ts
import { ComponentProps } from 'react'

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export const inertiaRender = async <T extends (props: any) => JSX.Element>(
  inertia: HttpContextContract['inertia'],
  component: T,
  props: ComponentProps<T>
) => {
  return inertia.render(component.name, props)
}
// resources/js/Pages/Home.tsx
const Home = ({ email }: { email: string }) => <h1>Hello {email}</h1>
// app/Controllers/Http/home/HomeController.ts
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Home from 'Resources/js/Pages/Home'
import { inertiaRender } from 'App/utils/inertiaRender'

export default class HomeController {
  public async index({ inertia, auth }: HttpContextContract) {
    const user = auth.user!

    return inertiaRender(inertia, Home, { email: user.email })
  }
}