| Package Data | |
|---|---|
| Maintainer Username: | acidjazz |
| Maintainer Contact: | acidjazz@gmail.com (kevin olson) |
| Package Create Date: | 2021-05-17 |
| Package Last Update: | 2026-01-27 |
| Language: | PHP |
| License: | MIT |
| Last Refreshed: | 2026-02-04 15:00:04 |
| Package Statistics | |
|---|---|
| Total Downloads: | 248,027 |
| Monthly Downloads: | 16,855 |
| Daily Downloads: | 651 |
| Total Stars: | 194 |
| Total Watchers: | 3 |
| Total Forks: | 30 |
| Total Open Issues: | 14 |
Model Typer is a powerful tool designed for developers working with Laravel and TypeScript. Its primary purpose is to simplify the generation of TypeScript interfaces from Laravel models, enhancing type safety and consistency in your applications.
Please read the upgrade guide here
Starting support is for Laravel >=v11.33.0 and PHP v8.2+
[!IMPORTANT] For Laravel >=10.43.0 || <11.33.0, use v2 instead
Require this package with composer using the following command:
composer require --dev fumeapp/modeltyper
Optionally, you can publish the config file using the following command:
php artisan vendor:publish --provider="FumeApp\ModelTyper\ModelTyperServiceProvider" --tag=config
You can simply run the following command to generate TypeScript interfaces:
php artisan model:typer
Or generate TypeScript types instead:
php artisan model:typer --use-types
The output is an accurate, type-safe representation of Laravel models in TypeScript, such as:
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;
posts: Posts;
// counts
teams_count: number;
posts_count: number;
// exists
teams_exists: boolean;
posts_exists: boolean;
}
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;
// counts
users_count: number;
// exists
users_exists: boolean;
}
export type Teams = Array<Team>;
export interface Post {
// columns
id: number;
user_id: number;
title: string;
content: string;
created_at?: Date;
updated_at?: Date;
// mutators
summary: string;
// relations
user: User;
comments: Comments;
// counts
comments_count: number;
// exists
comments_exists: boolean;
// sums
comments_sum_likes: number | null;
}
export type Posts = Array<Post>;
export interface Comment {
// columns
id: number;
post_id: number;
content: string;
created_at?: Date;
updated_at?: Date;
// mutators
summary: string;
// relations
post: Post;
}
export type Comments = Array<Comment>;
This command will go through all of your models and make TypeScript Interfaces based on the database columns, mutators, and relationships.
You can then pipe the output into your preferred ???.d.ts, or set the optional argument
output-file to generate it
[!TIP] To view the current mappings that are being used, use the following command:
php artisan model:typer-mappingsThese mappings can be extended or overridden in the config
By default, Model Typer generates TypeScript interfaces, but you can also generate type aliases using the --use-types option or by setting use-types to true in the config file.
export interface User {
id: number;
name: string;
email: string;
}
export type User = {
id: number;
name: string;
email: string;
};
When to use interfaces:
When to use types:
You can enable types in several ways:
# Via CLI option
php artisan model:typer --use-types
# Via config file
# Set 'use-types' => true in config/modeltyper.php
# For a specific model
php artisan model:typer --model=User --use-types
public function providers(): HasMany // <- this
{
return $this->hasMany(Provider::class);
}
protected function firstName(): Attribute
{
return Attribute::make(
get: fn (string $value): string => ucfirst($value), // <- this
);
}
Model Typer supports generating sum aggregates for Eloquent relationships. If a related model includes a summed value (e.g., the total number of likes on a user’s posts), this sum will be reflected in the generated output.
To enable this, define the sum relationship and target column using the $sums property on your model:
protected $sums = [
// Format: 'relationship' => 'column_to_sum'
'posts' => 'likes',
];
This will generate a corresponding field in the TypeScript interface:
export interface User {
...
// Sum of `likes` from related `posts`
posts_sum_likes: number | null;
}
This allows you to work with pre-calculated relationship totals directly in your frontend types.
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 will generate:
import { Point } from "@/types/api";
export interface Location {
// override
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";
// ...
}
You can override the default mappings provided by Model Typer or add new ones by publishing the config
Then inside custom_mappings add the Laravel type as the key and assign the TypeScript type as its value
You can also add mappings for your Custom Casts
'custom_mappings' => [
'App\Casts\YourCustomCast' => 'string | null',
'binary' => 'Blob',
'bool' => 'boolean',
'point' => 'CustomPointInterface',
'year' => 'string',
],
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
...
artisan model:typer --plurals
Exports for example, when a User model exists:
export type Users = User[];
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;
}
artisan model:typer --all
Exports both plurals & api-resources. i.e. it is equivalent to:
artisan model:typer --plurals --api-resources
Generate your interfaces for a single model
artisan model:typer --model=User
Generate your interfaces as JSON
artisan model:typer --json
Laravel lets you cast Enums in your models. This will get detected and bring in your enum class with your comments:
[!NOTE] ModelTyper uses Object Literals by default instead of TS Enums for opinionated reasons. But you can use
--use-enumsoption to use TS Enums instead of Object Literals.
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
...
}
[!NOTE] Notice how the comments are found and parsed - they must follow the specified format