Typing your frontend from the backend

In the past few months, we've been working on one of the biggest projects ever. This created some challenges to keep our frontend and backend types in sync.

In the past few months, we've been working on one of the biggest projects we've ever done. However, we had some experience with projects of this size. We knew this one would not only be heavy on the backend but also the frontend.

At Spatie, we fell in love with Inertia. The framework makes it so easy to create a backend Laravel application with a React frontend. Passing data between these sides is so straightforward. You can give them to a React component just like you would give them to a regular Blade view.

What made us fall in love with this setup is that we're now able to use TypeScript, which made our Javascript code type-checked and our frontend code less error-prone.

But with this type-checking, a new problem popped up. What about structures that would have to be used on both sides of the application? Take, for example, an enum like this one:

/**
 * @method static self guitar()
 * @method static self piano()
 * @method static self drums()
 */
class Instrument extends Enum
{
}
An enum using the spatie/enum package

This enum lives on the backend because we're going to need it to save a model, validate a request, or make some decisions further in the process. But what happens if we would need this enum on the frontend?

This enum's values can be communicated quite easily, get all the options(guitar, piano, and drums) and put them in an array with a corresponding label. Then we provide this array to the react component via Inertia at the end of our controller:

class MusiciansController
{
    public function show(Musician $musician)
    {
        return Inertia::render('Musician/Show', [
            'musician' => $musician,
            // the array we created
            'instrumentOptions' => Instrument::options(), 
        ]);
    }
}

On the frontend in our React component, we now have the following array with instruments:

[
	{'label': 'guitar', 'value': 'guitar'},
    {'label': 'piano', 'value': 'piano'},
    {'label': 'drums', 'value': 'drums'}
]

We want to show a select component where the musician can select its favorite instrument. We can pass this array to such a component, and we're done!

That's true for simple forms, but what about a situation where you want to show a checkbox to mark you are playing bass guitar when choosing guitar as an option?

That would require a condition like this:

if(instrument === 'guitar'){
	// Show checkbox
}

But actually, what's the type of this instrument variable? It's a string coming from the backend, and our frontend code doesn't know much more about it. We know this is no ordinary string. It's an enum of type Instrument, which means the string can only have three values: piano, guitar, and drums.

Luckily TypeScript can help us here. We can create an Instrument type:

export type Instrument = 'piano' | 'guitar' | 'drums';

When we now assign the instrument variable to value violin. The TypeScript type checker would then complain since instrument can only be set to piano, guitar, or drums.

This kind of type checking is cool! But there's a big catch. We're writing out types two times: one time in PHP and one time in TypeScript.

What will happen if we remove the guitar entry from the enum in PHP? It would still be in the TypeScript definition. Wich can break our code or give us incorrect type errors. Or what if we add a violin to our PHP Enum? This can go bad very quickly.

In small projects where one or two people are working on, this isn't such a big problem. You can communicate with your colleagues about these changes if the amount of types is small.

In this project, we worked with seven people, and these seven people would sometimes not work on the project for weeks. There we're backend developers who wouldn't write TypeScript and frontend developers who wouldn't write PHP. Even worse, we knew the number of types grow dramatically in the timespan of the project.

There were three options now:

  1. Type nothing; this is the worst option and was a no go for us.
  2. Try to keep these definitions in sync, but since this project was so big and we were working on it with many people, this was going to get out of hand.
  3. Try to keep these definitions in sync automatically.

Introducing Typescript Transformer

We've made a package for that third option, it's called laravel-typescript-transformer, and it will convert all the types from the backend that are needed in the frontend. The package will try to transform these types to TypeScript automatically. Let's take a look at it!

All we have to do is adding an @typescript annotation to our Instrument enum:

/**
 * @method static self guitar()
 * @method static self piano()
 * @method static self drums()
 * 
 * @typescript
 */
class Instrument extends Enum
{
}

Now when running:

php artisan typescript-transform

The package will create a generated.d.ts file in your Laravel resources directory containing:

export type Instrument = 'piano' | 'guitar' | 'drums';

Cool! When we add or remove or add an instrument from the enum, and this type of instrument is being used in our code, then the TypeScript checker will notice this and complain. This checker is running almost always, whenever someone is working on the frontend code. Or when we commit code, then a GitHub Action will also check the code, neat!

Our initial case was to transform enums and keep them in sync. This was so powerful we didn't stop there!

Let's say you have a data transfer object(DTO), an object with some public properties you can pass around in your application. In the past, we had to keep a type definition for this DTO in PHP and TypeScript. Wouldn't it be cool if we could have an automatically generated TypeScript definition for that object?

All you have to do is add the annotation:

/** @typescript */
class MusicianData extends DataTransferObject
{
    public string $name;
    
    public int $age;

    public Instrument $instrument;

    public static function create(array $data): self
    {
        return new self([
            'name' => $data['name'],
            'age' => $data['age'],
            'instrument' => Instrument::make($data['age'])
        ]);
    }
}
A DTO using the spatie/data-transfer-object package

Now when running the Typescript Transformer command, our generated TypeScript file looks like this:

export type Instrument = 'piano' | 'guitar' | 'drums';

export type MusicianData = {
    name : string;
    age: number;
    instrument: Instrument;
}

We use this DTO for two purposes:

  • When we would give the Musician model to a form in the frontend, so we can build a form for creating/editing the model
  • When that form is filled in, and we receive the data from the frontend, which we will use in the backend

In our project, we stopped using the default Laravel resources in favor of DTO resources that also could be typed on the frontend. In the package documentation, we've added a whole section on how you could accomplish this.

We even use these resources to log activity on models, which is very powerful but something for another blog post.

How does it work?

In essence, the process of converting PHP classes to Typescript is relatively easy. Let's take a look at the steps:

Searching for PHP classes that need to be converted
The package will look in a directory you defined in your config for classes with a @typescript annotation and make a list. It is possible to add classes to that list without @typescript annotation automatically, you can read more about it here.

Finding a transformer for the class
Transformers are the heart of the package. They will take a PHP class and output a TypeScript representation. We've included some essential transformers in the package, but you probably also want to write your own transformers.

Replacing missing symbols
Each transformer will output a TypeScript representation, but what about types that link to other types? For example, in our MusicianData, we link to the Instrument enum. At the time of transforming, we do not know what the TypeScript type of Instrument will be, so we cannot add the type to the MusicianData type (yet).

That's why each transformer will keep track of a list of links to other types it does not know yet. In this step, we replace these missing links with the correct types.

Write out the generated TypeScript
In this last step, we'll write down the transformed types into one TypeScript file that you defined in your config.

That's it! Although this transformation looks relatively straightforward, there's a lot more to look at. For example, you can create inline types, or use another name for the TypeScript type or completely modify the types of the properties of DTO's as you wish.

Be sure to take a look at the laravel-typescript-transformer package. We've also made a typescript-transformer package that's not Laravel specific, which is the basis for the Laravel package and can be used in any PHP project.

The future

Although the initial version is just released, I've already some things on my list for the second version of the package:

  • A watcher which automatically transforms types as they are changed
  • Support for nullable TypeScript types
  • Support for omitting namespaces in TypeScript types