inertiajs-adonisjs icon indicating copy to clipboard operation
inertiajs-adonisjs copied to clipboard

feat(types): add InertiaPage type helper

Open codytooker opened this issue 1 year ago • 7 comments

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
}

codytooker avatar Mar 06 '23 01:03 codytooker

Great work. Can you please update the readme?

eidellev avatar Mar 07 '23 06:03 eidellev

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
     }
}

codytooker avatar Mar 07 '23 16:03 codytooker

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

eidellev avatar Mar 07 '23 16:03 eidellev

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

codytooker avatar Mar 07 '23 16:03 codytooker

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

eidellev avatar Mar 07 '23 17:03 eidellev

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.

codytooker avatar Mar 08 '23 16:03 codytooker

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 })
  }
}

mle-moni avatar Aug 31 '23 14:08 mle-moni