spatie/typescript-transformer

repository·main·Indexed 19 days ago

https://github.com/spatie/typescript-transformer

A tool for converting PHP classes, enums, and other structures into TypeScript types to ensure type safety between PHP backends and TypeScript frontends. It uses a pipeline of search, reflection, transformation, filtering, and writing to generate types, supporting custom writers, loggers, and transformers.

Tokens
32K
Snippets
101
Records
119
Agent score
64%

What's inside spatie/typescript-transformer

  1. Transform PHP classes and enums to TypeScript

    main

    The spatie/typescript-transformer package converts PHP structures like classes and enums into TypeScript types.

    To mark a class for transformation, use the #[TypeScript] attribute. The transformer maps PHP types to their TypeScript equivalents (e.g., int to number, nullable types to unions with null).

    For string-backed enums, the transformer generates a TypeScript union type of the enum values.

    #[TypeScript]
    class User
    {
        public int $id;
        public string $name;
        public ?string $address;
    }

    Converts to:

    export type User = {
        id: number;
        name: string;
        address: string | null;
    }

    And enums:

    enum Languages: string
    {
        case TYPESCRIPT = 'typescript';
        case PHP = 'php';
    }

    Converts to:

    export type Languages = 'typescript' | 'php';
  2. Use Laravel-specific type transformations

    main

    The Laravel package provides automatic configuration for several common Laravel types via the LaravelTypeScriptTransformerExtension. These include:

    • Carbon/CarbonInterface: Replaced with string in TypeScript.
    • Collections: The AttributedClassTransformer is replaced with LaravelAttributedClassTransformer, which handles Laravel's Collection and EloquentCollection as array-like structures.
    • Pagination: TypeScript types are generated for LengthAwarePaginator and CursorPaginator, including the full structure of data, links, and meta properties.

    These features are automatically loaded if you use the Laravel Data or controllers extensions. If you want these base Laravel types without using those extensions, you must add the extension manually in your TypeScriptTransformerServiceProvider.

    use Spatie\LaravelTypeScriptTransformer\LaravelTypeScriptTransformerExtension;
    
    protected function configure(TypeScriptTransformerConfigFactory $config): void
    {
        $config->extension(new LaravelTypeScriptTransformerExtension());
    }
  3. How Laravel Data classes are transformed

    main

    The extension automatically picks up all public properties from your Data classes and resolves their types. This includes support for complex types defined via docblocks. A standard Data class with public properties will be converted into a corresponding TypeScript type where PHP types (like string, int, ?string) are mapped to TypeScript types (like string, number, string | null).

    use Spatie\LaravelData\Data;
    
    class UserData extends Data
    {
        public function __construct(
            public string $name,
            public string $email,
            public int $age,
            public ?string $avatar,
        ) {
        }
    }

    Generates:

    export type UserData = {
        name: string;
        email: string;
        age: number;
        avatar: string | null;
    };
  4. How watch mode works and its limitations

    main

    Watch mode monitors your PHP files and automatically regenerates TypeScript files upon detection of changes.

    Architecture

    • Master Process: Remains running continuously.
    • Worker Process: Handles the transformation. It is restarted when a change requires a full application reload.
    • Smart Swapping: To avoid full application reloads on every change, the package uses wrapper classes to swap Reflection instances. It uses PHP's Reflection API for initial data and roave/better-reflection to create in-memory representations of changed classes.

    Important Limitations

    • Experimental Status: This feature is heavily experimental and may not work in all environments.
    • Application State: If your custom provider relies on external application state (e.g., checking a service container or application routes), the worker process must be restarted to reflect those changes, as the smart swapping only handles Reflection-based class changes.
  5. Exclude hidden properties

    main

    Properties marked with the #[Hidden] attribute (from either the spatie/typescript-transformer or spatie/laravel-data packages) will be excluded from the generated TypeScript type.

    use Spatie\LaravelData\Data;
    use Spatie\LaravelData\Attributes\Hidden;
    
    class UserData extends Data
    {
        public function __construct(
            public string $name,
            #[Hidden]
            public string $password,
        ) {
        }
    }

    Generates:

    export type UserData = {
        name: string;
    };
  6. Map property names using Laravel Data attributes

    main

    The extension respects Laravel Data's #[MapOutputName] and #[MapName] attributes. If a property has a mapped output name, the TypeScript property will use that mapped name instead of the PHP property name.

    use Spatie\LaravelData\Data;
    use Spatie\LaravelData\Attributes\MapOutputName;
    
    class UserData extends Data
    {
        public function __construct(
            #[MapOutputName('full_name')]
            public string $name,
            public string $email,
        ) {
        }
    }

    Generates:

    export type UserData = {
        full_name: string;
        email: string;
    };
  7. Understand how PHP types are transformed to TypeScript

    main

    The TypeScript transformer automatically maps basic PHP types to their TypeScript equivalents.

    Basic Type Mapping

    • string $\rightarrow$ string
    • int or float $\rightarrow$ number
    • bool $\rightarrow$ boolean
    • mixed $\rightarrow$ any
    • object $\rightarrow$ object

    Nullability, Unions, and Intersections

    • Nullable types: ?string becomes string | null.
    • Unions: string | int becomes string | number.
    • Intersections: string & int becomes string & number.

    Array Transformations

    Arrays are transformed based on their annotations:

    • Unannotated: array becomes Array.
    • Integer keys: /** @var array<int, bool> */ becomes Array<boolean>.
    • String keys: /** @var array<string, bool> */ becomes Record<string, boolean>.
    • Enums as keys: /** @var array<PostType, string> */ becomes Record<'news'|'blog', string>.
    • Shapes: /** @var array{age: int, name: string} */ becomes { age: number, name: string }.
    • Generics: /** @var Collection<int, string> */ becomes Illuminate.Support.Collection<number, string>.
    class Types
    {
        public string $property; // string
        public int $property; // number
        public float $property; // number
        public bool $property; // boolean
        public mixed $property; // any
        public object $property; // object
        public ?string $property; // string | null
        public string | int $property; // string | number
        public string & int $property; // string & number
    }
  8. How the TypeScript Transformer works

    main

    The TypeScript transformer follows a five-step pipeline to convert PHP code into TypeScript types:

    1. Search: It scans your application for PHP classes.
    2. Reflection: It creates a ReflectionClass for every discovered class.
    3. Transformation: It passes these ReflectionClass instances through a list of registered Transformers. Each transformer attempts to convert the class into a TypeScript type.
    4. Filtering: If a transformer successfully processes a class, it is added to a list for writing; otherwise, the class is ignored.
    5. Writing: The collected list of transformed types is written to TypeScript files using a configured Writer.

    Transformers are executed in the order they are registered. If one transformer cannot handle a specific class, the framework moves to the next one in the list.

  9. How TypeScript nodes work

    main

    The package uses a tree of TypeScript nodes to represent TypeScript types internally. These nodes can be composed to build complex type structures, such as aliases, objects, unions, and primitives. You can use these nodes to programmatically define types that the transformer will then output as valid TypeScript code.

    All available nodes are located in the Spatie\TypeScriptTransformer\TypeScript namespace.

    use Spatie\TypeScriptTransformer\TypeScriptNodes;
    
    new TypeScriptAlias(
        new TypeScriptIdentifier('User'),
        new TypeScriptObject([
            new TypeScriptProperty('id', new TypeScriptNumber()),
            new TypeScriptProperty('name', new TypeScriptString()),
            new TypeScriptProperty('address', new TypeScriptUnion([
                new TypeScriptString(),
                new TypeScriptNull(),
            ])),
        ]),
    );
  10. How PHP Nodes and the PhpNodeCollection work

    main

    The TypeScript transformer uses a PhpNodeCollection to maintain a persistent registry of PhpClassNode objects. These nodes contain PHP reflection data for classes, interfaces, and enums, keyed by their Fully Qualified Class Name (FQCN).

    This collection is essential for maintaining state during watch mode: when a PHP file changes, the PhpNodeCollection automatically updates the corresponding PhpClassNode with fresh reflection data.

    Important Lifecycle Note: The collection only tracks classes that have been either:

    1. Transformed by a transformer.
    2. Manually added by a provider.

    Because of this, the collection may be empty during the very first run of a provider if the transformers have not yet executed.

  11. How Laravel Controller TypeScript objects work

    main

    The transformer converts Laravel controller actions into callable TypeScript functions. Each function returns an object containing the url and the HTTP method.

    Basic Usage

    For a standard controller, import the controller class and call the action method:

    import { PostsController } from './controllers';
    
    const { url, method } = PostsController.index();
    // { url: '/posts', method: 'get' }

    Route Parameters

    Actions with route parameters require an object containing those parameters as the first argument:

    const { url, method } = PostsController.show({ post: 1 });
    // { url: '/posts/1', method: 'get' }

    Query Parameters

    You can pass query parameters via an options object containing a query key:

    const { url } = PostsController.index({ query: { page: 2, per_page: 15 } });
    // '/posts?page=2&per_page=15'

    Multiple HTTP Methods

    If an action is registered for multiple methods, the first one registered is the default. You can access specific variants using the method name:

    PostsController.update({ post: 1 }); // Default (e.g., 'put')
    PostsController.update.patch({ post: 1 }); // Explicit 'patch'

    Invokable Controllers

    Controllers with a single __invoke method are generated as a single callable rather than an object with methods:

    import { ShowDashboardController } from './controllers';
    
    const { url, method } = ShowDashboardController();
  12. How Request types are resolved

    main

    Request types are detected by looking for a method parameter that is a Data object (from spatie/laravel-data). The first matching parameter becomes the Request type.

    Example PHP signature:

    public function store(StorePostData $data): PostData
    {
        // ...
    }

    If no Data object parameter is found, the request type defaults to object. Support for Laravel FormRequest classes is planned for a future release.