splitstack/laravel-typewriter

Unified TypeScript tooling for Laravel: DataPacket transport envelope, DTO/VO type generation, and Wayfinder route types in a single command.

Maintainers

Package info

github.com/EmilienKopp/laravel-typewriter

pkg:composer/splitstack/laravel-typewriter

Transparency log

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

v0.1.0 2026-07-15 00:52 UTC

This package is auto-updated.

Last update: 2026-07-22 03:24:33 UTC


README

DDD-friendly unified TypeScript tooling for Laravel. One command generates:

  • DataPacket types — a typed Back-to-Front transport envelope with defaults, metadata, and source/target routing
  • DTO / Value Object types — from laravel-data Data classes, plain DTOs, and value objects
  • Route types — via laravel/wayfinder (optional)

Powered by spatie/laravel-typescript-transformer under the hood.

Installation

composer require splitstack/laravel-typewriter

Publish the config:

php artisan vendor:publish --tag=typewriter-config

Publish the TypeScript DataPacket utility class:

php artisan vendor:publish --tag=typewriter-ts
# → resources/js/utils/DataPacket.ts

The DataPacket pattern

DataPacket is a typed envelope for passing data from PHP to the frontend. It gives you:

  • Typed payload (data) — a laravel-data Data object, or an untyped array for convenience
  • Per-key defaults (defaults) — validated against the keys in data so stale defaults can't sneak in
  • Metadata (meta) — out-of-band context (pagination, permissions, flags)
  • Global fallback (default) — a single value for when the whole packet has no specific match
  • Source / target routing (source, target) — BackedEnum scalars that tell the frontend where data came from and where it's going, without magic strings on either side

Convenience mode (untyped)

Use DataPacket directly when you don't need end-to-end type safety:

use Splitstack\Typewriter\DataPacket;

return inertia('Orders/Index', [
    'orders' => DataPacket::from([
        'data'   => $orders->toArray(),
        'meta'   => ['total' => $orders->total()],
        'source' => 'orders',
    ]),
]);

TypeScript receives DataPacket where data is Record<string, unknown>.

Typed mode (end-to-end type safety)

Scaffold a typed packet + its companion Data class:

php artisan make:packet OrderPacket --data=OrderData

This creates two files:

// app/Domain/Data/OrderData.php
#[TypeScript]
class OrderData extends Data
{
    public function __construct(
        public readonly int $id,
        public readonly string $status,
        public readonly float $total,
    ) {}
}

// app/Domain/Packets/OrderPacket.php
class OrderPacket extends DataPacket
{
    public function __construct(
        public readonly OrderData $data,
        array $meta = [],
        array $defaults = [],
        mixed $default = null,
        string|int|null $source = null,
        string|int|null $target = null,
    ) {
        parent::__construct($data->toArray(), $meta, $defaults, $default, $source, $target);
    }
}

Use it in your controller:

return inertia('Orders/Show', [
    'order' => OrderPacket::from([
        'data'    => OrderData::from($order),
        'defaults'=> ['status' => 'pending'],
        'source'  => OrderSource::Api->value,
        'target'  => OrderTarget::Checkout->value,
    ]),
]);

TypeScript consumption

import type { OrderPacket } from '@/types/packets'
import { DataPacket } from '@/utils/DataPacket'

const packet = DataPacket.from<OrderPacket['data']>(props.order)

packet.get('status')        // OrderData['status'] — falls back to defaults if null
packet.raw('status')        // raw value, no default resolution
packet.isFrom('api')        // source routing check
packet.isFor('checkout')    // target routing check
packet.getMeta('total')     // metadata access
packet.fallback()           // global default value

Type generation

Source groups

Configure which directories to scan in config/typewriter.php:

'sources' => [
    'value_objects' => [
        'directories' => ['Domain/ValueObjects'],
    ],
    'dtos' => [
        'directories' => ['Domain/DTOs', 'Http/Resources'],
    ],
    'packets' => [
        'directories' => ['Domain/Packets', 'Http/Packets'],
        'enums'       => ['Domain/Enums'],  // BackedEnums used as source/target
        'enabled'     => true,
    ],
],

Transformer chain

For each source group, typewriter runs a single typescript-transformer pipeline:

Priority Transformer Handles
1 DataPacketTransformer Typed DataPacket subclasses
2 DataClassTransformer laravel-data Data classes
3 EnumTransformer BackedEnums (source/target routing)
4 PlainClassTransformer Plain VOs, DTOs, any non-abstract class

Classes that no transformer matches are silently skipped.

Commands

# Everything: wayfinder routes + all source groups
php artisan typewriter:generate

# Types only (skip wayfinder)
php artisan typewriter:generate --types-only

# Individual source groups
php artisan typewriter:typegen --value-objects-only
php artisan typewriter:typegen --dtos-only
php artisan typewriter:typegen --packets-only

# Skip barrel index.ts
php artisan typewriter:typegen --no-barrel

Output

resources/js/types/
├── value-objects.ts
├── dtos.ts
├── packets.ts        ← DataPacket types + routing enums
└── index.ts          ← barrel re-export

Marking classes for export

laravel-data Data classes — add #[TypeScript]:

use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript]
class OrderData extends Data { ... }

Plain classes — no attribute needed. Any public typed property or @property PHPDoc tag in a scanned directory is picked up automatically.

DataPacket subclasses — no attribute needed. The DataPacketTransformer detects them by class hierarchy.

BackedEnums — put them in the enums directories of the packets source group:

enum OrderSource: string
{
    case Api = 'api';
    case Admin = 'admin';
}

Generated output:

export type OrderSource = 'api' | 'admin';

Configuration reference

// config/typewriter.php
return [
    'sources' => [
        'value_objects' => [
            'directories' => ['Domain/ValueObjects'],
            'include'     => ['*'],   // glob patterns
            'exclude'     => [],
        ],
        'dtos' => [
            'directories' => ['Domain/DTOs', 'Http/Resources'],
        ],
        'packets' => [
            'directories' => ['Domain/Packets', 'Http/Packets'],
            'enums'       => ['Domain/Enums'],
            'enabled'     => false,   // opt-in; enable once you have packets
        ],
    ],

    'output' => [
        'value_objects' => null,  // resource_path('js/types/value-objects.ts')
        'dtos'          => null,  // resource_path('js/types/dtos.ts')
        'packets'       => null,  // resource_path('js/types/packets.ts')
        'barrel'        => null,  // resource_path('js/types/index.ts')
    ],

    'ts_asset_path' => null, // resource_path('js/utils/DataPacket.ts')
];

Requirements

License

MIT