fumeapp/modeltyper

Generate TypeScript interfaces from Laravel Models


README

68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f7468756d622f392f39612f4c61726176656c2e7376672f3132303070782d4c61726176656c2e7376672e706e67 68747470733a2f2f6d69726f2e6d656469756d2e636f6d2f6d61782f3831362f312a6d6e36624f733773365162616f3135504d4e52794f412e706e67

Model Typer

Generate TypeScript interfaces from Laravel Models

Packagist License Latest Stable Version Total Downloads

composer require --dev fumeapp/modeltyper
php artisan model:typer

will output

export interface User {
    // columns
    id: number;
    email: string;
    name: string;
    created_at?: Date;
    updated_at?: Date;
    // mutators
    first_name: string;
    initials: string;
    // relations
    teams: Teams;
}
export type Users = Array<User>;

export interface Team {
    // columns
    id: number;
    name: string;
    logo: string;
    created_at?: Date;
    updated_at?: Date;
    // mutators
    initials: string;
    slug: string;
    url: string;
    // relations
    users: Users;
}
export type Teams = Array<Team>;

What does this do?

This command will go through all of your models and make TypeScript Interfaces based on the columns, mutators, and relationships. You can then pipe the output into your preferred ???.d.ts

Requirements

Starting support is for Laravel v10+ and PHP v8.1+

Note This package may require you to install Doctrine DBAL. If so you can run

composer require doctrine/dbal
  1. You must have a return type for your model relationships
public function providers(): HasMany // <- this
{
    return $this->hasMany(Provider::class);
}
  1. You must have a return type for your model mutations
public function getFirstNameAttribute(): string // <- this
{
    return explode(' ', $this->name)[0];
}

Additional Options

--model= : Generate typescript interfaces for a specific model
--global : Generate typescript interfaces in a global namespace named models
--json : Output the result as json
--plurals : Output model plurals
--no-relations : Do not include relations
--optional-relations : Make relations optional fields on the model type
--no-hidden : Do not include hidden model attributes
--timestamps-date : Output timestamps as a Date object type
--optional-nullables : Output nullable attributes as optional fields
--api-resources : Output api.MetApi interfaces
--resolve-abstract : Attempt to resolve abstract models)
--fillables : Output model fillables
--fillable-suffix=fillable
--all : Enable all output options (equivalent to --plurals --api-resources)'

Custom Interfaces

If you have custom interfaces you are using for your models you can specify them in a reserved interfaces array

For example for a custom Point interface in a Location model you can put this in the model

public array $interfaces = [
    'coordinate' => [
        'import' => "@/types/api",
        'type' => 'Point',
    ],
];

And it should generate:

import { Point } from "@/types/api";

export interface Location {
    // columns
    coordinate: Point;
}

This will override all columns, mutators and relationships

You can also specify an interface is nullable:

    public array $interfaces = [
        'choices' => [
            'import' => '@/types/api',
            'type' => 'ChoicesWithPivot',
            'nullable' => true,
        ],
    ];

You can also choose to leave off the import and just use the type:

    public array $interfaces = [
        'choices' => [
            'type' => "'good' | 'bad'",
        ],
    ];

And it should generate:

export interface Location {
    // columns
    choices: "good" | "bad";
}

Using the custom interface is also a good place to append any additional properties you want to add to the interface.

For example, if your interface keeps some additional state in something like Vuex, you can add it to the interfaces:

    public array $interfaces = [
        'state' => [
            'type' => "found' | 'not_found' | 'searching' | 'reset'",
        ],
    ];

This will generate:

export interface Location {
    // ...
    // overrides
    state: "found" | "not_found" | "searching" | "reset";
    // ...
}

Declare global

Generate your interfaces in a global namespace named model

artisan model:typer --global
export {}
declare global {
  export namespace models {

    export interface Provider {
      // columns
      id: number
      user_id: number
      avatar?: string
...

Output plural interfaces for collections

artisan model:typer --plurals

Exports for example, when a User model exists:

export type Users = User[]

Output Api.MetApi* resources

artisan model:typer --api-resources

Exports:

export interface UserResults extends api.MetApiResults { data: Users }
export interface UserResult extends api.MetApiResults { data: User }
export interface UserMetApiData extends api.MetApiData { data: User }
export interface UserResponse extends api.MetApiResponse { data: UserMetApiData }

Enable all output options

artisan model:typer --all

Exports both plurals & api-resources. i.e. it is equivalent to:

artisan model:typer --plurals --api-resources

Laravel V9 Attribute support

Laravel now has a very different way of specifying accessors and mutators. In order to tell modeltyper the types of your attributes - be sure to add the type the attribute returns:

    /**
     * Determine if the user is a captain of a team
     *
     * @return Attribute
     */
    public function isCaptain(): Attribute
    {
        return Attribute::make(
            get: fn (): bool => $this->teams[0]->pivot->captain ?? false,
        );
    }

This will generate something like:

export interface User {
    // columns
    id: number;
    email: string;
    name?: string;
    created_at?: Date;
    updated_at?: Date;
    // mutators
    is_captain: boolean;
    // relations
    teams: TeamUsers;
}

For Single Model

Generate your interfaces for a single model

artisan model:typer --model=User

Output as JSON

Generate your interfaces as JSON

artisan model:typer --json

Enum Eloquent Attribute Casting

Laravel now lets you cast Enums in your models. This will get detected and bring in your enum class with your comments:

app/Enums/UserRoleEnum.php

<?php

namespace App\Enums;

/**
 * @property ADMIN - Can do anything
 * @property USER - Standard read-only
 */
enum UserRoleEnum: string
{
    case ADMIN = 'admin';
    case USER = 'user';
}

Then inside our User model

app/Models/User.php

protected $casts = [
    'role' => App\Enums\UserRoleEnum::class,
];

Now our modeltyper output will look like the following:

const UserRoleEnum = {
  /** Can do anything */
  ADMIN: 'admin',
  /** Standard read-only */
  USER: 'user',
}
export type UseRoleEnum = typeof UseRoleEnum[keyof typeof UserRoleEnum]
export interface User {
  ...
  role: UserRoleEnum
  ...
}

ModelTyper uses Object Literals instead of TS Enums for opinionated reasons

Notice how the comments are found and parsed - they must follow the specified format