---
url: /ts/analyzer-api.md
---
# Analyzer API

The analysis that the [Laravel TypeScript Publisher](https://github.com/abetwothree/laravel-ts-publish) runs on your classes is also available to your own code. `AstEngine::analyze()` takes a class and a method name. It returns the typed properties that method returns, plus the imports those types need. Use it when your own code needs this package's types without a full publish, such as a custom Artisan command or another package.

To change what `ts:publish` itself does, replace one of its stages instead. See [Customizing the Pipeline](./customizing-the-pipeline.md).

## Analyzing a Method

`analyze()` reads what a method returns: nested arrays, conditionals, closures, and method calls. It works on any class, whether or not it's a resource. A property read or a method call is typed from the class of the value you call it on, such as `$this->author->name` or a local variable. A plain value object gets the same types a resource does for enum `->value` and `->name` reads, `(object)` and `(array)` casts, helpers that return the array you pass them, and an inherited or trait method.

Laravel's conditional methods, such as `$this->when()` and `$this->whenLoaded()`, belong to `JsonResource`, so `analyze()` reads them that way only on a resource. On any other class, `$this->when()` is that class's own method. Laravel's `Conditionable::when()`, which events and other classes can use, returns the callback's result or the object itself. The package doesn't type that, so the key comes back `unknown`, as it does in a publish. A resource built around the call is the exception: `UserResource::make($this->when(…))` still comes back optional on any class, though `Conditionable::when()` always sends the key. Write such a key as a ternary, or give it a `#[TsCasts]` entry on the method with `'optional' => false`.

This example analyzes a service class's `toPayload()` method:

```php
use AbeTwoThree\LaravelTsPublish\Ast\AstEngine;

$result = resolve(AstEngine::class)->analyze(App\Services\CartSummary::class, 'toPayload', null, 'app/services');

// $result->properties   => list<{name, type, optional, description}>
// $result->typeImports  => import path => list<type name>
// $result->valueImports => import path => list<enum const name>
```

`analyze()` takes four arguments:

| Argument             | Default     | Description                                                                                                                                                                                                                                            |
| -------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `$class`             | required    | The class to analyze.                                                                                                                                                                                                                                  |
| `$method`            | `'toArray'` | The method whose return value is analyzed.                                                                                                                                                                                                             |
| `$modelClass`        | `null`      | The Eloquent model that `$this->...` reads resolve against. Leave it `null` to bind no model, or to let a `JsonResource` subclass find its own.                                                                                                        |
| `$fromNamespacePath` | `''`        | The namespace path of the file you're writing, such as `app/services`. Import paths come back relative to it, as [Automatic Relative Imports](./modular-publishing.md#automatic-relative-imports) describes. Pass `''` for a file at your output root. |

`analyze()` writes nothing to disk. The file, its formatting, and any barrel entry are up to you.

::: warning `analyze()` Is the Whole Public API
`analyze()` and the `AnalysisResult` it returns are the only supported parts of the engine. Every other class under `AbeTwoThree\LaravelTsPublish\Ast`, and every other method on `AstEngine`, is `@internal`. Those change without notice, so code that calls them can break on any update.
:::

### `AnalysisResult`

`analyze()` returns a readonly `AnalysisResult` with three properties:

```php
public function __construct(
    public array $properties,
    public array $typeImports,
    public array $valueImports,
) {}
```

Each property holds one part of the module you write:

* **`properties`**: a `list<{name, type, optional, description}>` with one entry per key the method returns. `type` is already a TypeScript type string. `optional` is `true` wherever the code can leave the key out, such as a conditional method or a `mergeWhen()`.
* **`typeImports`**: an `import path => list<name>` map of the types that the property types name. Write these as `import type` lines.
* **`valueImports`**: an `import path => list<name>` map of the enum constants that an `AsEnum<typeof X>` wrapper reads. Write these as plain `import` lines, never as `import type`.

Two classes that share an import path merge into one entry, so neither overwrites the other.

The three properties agree with each other, so a module that renders all three as they are compiles:

* **Same-name classes**: two classes with the same short name, such as two `User` models, come back under separate aliases, and the property types use those aliases.
* **`EnumResource` properties**: an `EnumResource::make()` property comes back as `AsEnum<typeof X>`, beside the value import that wrapper needs. With `enums.use_tolki_package` off, it comes back as the plain enum type with a type import, and `valueImports` stays empty.
* **No unused imports**: nothing is imported that no property type names. A name counts wherever it appears in a type, even inside a string, template text, or a comment. A method-level `#[TsCasts]` of `"'User' | 'Admin'"` therefore keeps an unused `User` import, which TypeScript reports only under `noUnusedLocals`.

### Method Parameters

On a class that isn't a `JsonResource`, `analyze()` reads a parameter typed as `Illuminate\Http\Request`, or a subclass of it, as the request. A call such as `$request->url()` then types as `string`. On a `JsonResource`, it doesn't, so `$request->url()` in `toArray()` is `unknown`.

`analyze()` binds no other parameters. A [model metadata](./model-metadata.md) provider shows the difference. `analyze($provider, 'provide')` leaves `$model->getTable()` as `unknown`, while the published companion types it as `string`, because a publish binds the `Model $model` parameter to its declared type. The published companion also applies the `@return` docblock and `#[TsCasts]` on top.

There's no public entry point for an analysis that binds other parameters.

## Analyzing Resources, Models, and Events

A resource works with no extra arguments. A model needs one more, and a broadcast event needs the method name.

### Resources

Call `analyze()` with a `JsonResource` subclass and leave the other arguments at their defaults. The package finds the resource's model the same way `ts:publish` does, and analyzes `toArray()`:

```php
$result = resolve(AstEngine::class)->analyze(App\Http\Resources\PostResource::class);
```

`analyze()` doesn't use a custom `resources.transformer_class`, so a [`resolveModelClass()` override](./customizing-the-pipeline.md#changing-how-a-resource-finds-its-model) doesn't apply here. To choose the model yourself, pass it as the third argument.

Every pattern in [API Resources](./api-resources.md) gives the same properties, imports, and optionality as a publish. That includes the `when()` family of conditional methods, `EnumResource::make()`, nested and collection resources, `merge()` and `mergeWhen()`, and relation filters such as `$this->author->only([...])`. The exceptions are a `morphTo` union, a collection that publishes as an alias, class-level and model-level `#[TsCasts]`, a `toResource()` guess by naming convention, and the filled index signature of an interpolated key. [What It Cannot Do](#what-it-cannot-do) covers each one.

A [JSON:API resource](./api-resources.md#json-api-resources) returns its resource object, the `id`, `type`, `attributes`, and other members a publish writes, even when it declares a `toArray()`.

To read a resource's `with()`, `resolve()`, or `jsonSerialize()`, pass that method's name as the second argument. `analyze($resource, 'with')` starts from the resource's `$with` property, so a `with()` that returns `parent::with($request)`, or builds on it, returns the `$with` keys. A `resolve()` or `jsonSerialize()` built on the parent's method returns the resource's `toArray()` keys, as Laravel sends them, and a `jsonSerialize()` reads the resource's own `resolve()` when it declares one.

### Models

A model needs its own class as the third argument. The automatic model lookup runs only for a `JsonResource` subclass, and a model's `toArray()` is Laravel's own method, with nothing in its body to read. Pass the model twice to get the attribute and relation shape that [Models](./models.md) publishes from:

```php
$result = resolve(AstEngine::class)->analyze(App\Models\User::class, 'toArray', App\Models\User::class);
```

If you leave the third argument `null`, all three properties come back empty.

### Broadcast Events

`analyze($event, 'broadcastWith')` returns the payload the package publishes for that event, as [Broadcast Data](./broadcast-events.md#broadcast-data) describes. A `broadcastWith()` inherited from a parent class or supplied by a trait counts, as it does when Laravel dispatches the event. The result doesn't include two things a publish adds on top: the event's class-level `#[TsCasts]` overrides, and `Partial<Model>` for a model property.

An event with no `broadcastWith()` anywhere in its class hierarchy is published from its public properties. `analyze()` never falls back to them, so on such an event it returns an empty result, imports included. Only `ts:publish` produces that shape.

## What It Cannot Do

Some shapes come only from a full `ts:publish` run. In these cases, `analyze()` returns something different from the published file, or nothing at all:

**Inertia page props.** [Page props](./routing.md#inertia-integration) come from the props argument of an `Inertia::render()` call in a controller action, not from a method's return value. `analyze()` on a controller action returns that method's own return shape, not its page props. There's no public entry point for page props.

**Inertia shared data.** `analyze($middleware, 'share')` returns what inference finds in `share()`. It also leaves out a `#[TsCasts]` on `share()`, though `analyze()` applies one on any other method it analyzes. The published `SharedData` applies `#[TsCasts]` first, then the `@return array{...}` docblock on `share()`, then inference, as [Type Resolution Priority](./inertia.md#type-resolution-priority) describes. It also drops the `errors` key that `Inertia\Middleware::share()` adds, because `@inertiajs/core` types `page.props.errors` itself.

For example, a `share()` whose docblock declares `filters?: array<string, string>` and whose body returns `(array) $request->query('filters', [])` comes back from `analyze()` as `filters: unknown[]`. The published `SharedData` has `filters?: Record<string, string>`.

**Form request rules.** A form request's published interface is built from its rules by the class in `form_requests.analyzer_class`, not by `analyze()`. See [Form Requests](./form-requests.md). `analyze($request, 'rules')` types the array that `rules()` returns, not the validated payload.

**`morphTo` unions.** `analyze()` can't list the models a `morphTo` relation can hold, because only a publish scans your models for the `morphOne` and `morphMany` relations that point back. The relation is left out of a model's shape, and it comes back `unknown` where a `toArray()` names it. A [`@return MorphTo<A|B, $this>` docblock](./models.md#typing-morphto-relations) on the relation method gives both `analyze()` and a publish the union.

**Collections that publish as an alias.** A `ResourceCollection` with no keys beyond its items publishes as a flat alias, `export type X = Y[]`, rather than an interface, when it has `$wrap = null`. After [`JsonResource::withoutWrapping()`](./api-resources.md#the-wrap-key-your-app-sets), so does every collection with Laravel's default shape that doesn't redeclare `$wrap`. An alias has no property list or imports to put in an `AnalysisResult`, so all three properties come back empty. `ts:publish` writes the alias.

**Class-level and model-level `#[TsCasts]`.** `analyze()` applies a `#[TsCasts]` on the analyzed method only. `ts:publish` then applies a resource's class-level `#[TsCasts]` and its model's `#[TsCasts]`, types and optionality both. On a `CommentResource` whose class-level casts name `flagged_at` and `metadata`, `analyze()` returns `flagged_at: string | null` and `metadata: unknown[] | null`. The published interface has `flagged_at?: string | null` and `metadata: Record<string, unknown>`.

**Resources guessed by naming convention.** When `toResource()` guesses a resource class by its name, a publish uses the guess only if that resource is published too. `analyze()` accepts any resource class that exists. Where a publish that excludes the guessed resource writes `unknown`, `analyze()` names the resource.

**Filled index signatures.** A `@return` docblock can fill in the value type of an interpolated key's index signature. `analyze()` checks that fill only against the keys the method returns. `ts:publish` also checks it against the extends clause and the `#[TsCasts]` keys: a resource's class-level and model-level ones, or an event's class-level ones. Wherever they could conflict, it resets the fill to `unknown | undefined`, and under an extends clause it always does. See [Interpolated Keys](./api-resources.md#interpolated-keys).

On a `#[TsExtends]` resource, `analyze()` returns ``[key: `${string}_tag`]: string | undefined`` where the published interface has `unknown | undefined`.

The difference can also run the other way. Where a key the method returns can't join the fill, `analyze()` returns `unknown | undefined`. Once a class-level or model-level `#[TsCasts]` entry gives that key a type that can join, `ts:publish` joins the fill with it. On a resource whose `_tag` keys only a `@return array<string, string>` docblock types, beside a `main_tag` that only a class-level entry `'main_tag' => 'number'` types, `analyze()` returns ``[key: `${string}_tag`]: unknown | undefined``, and the published interface has ``[key: `${string}_tag`]: string | number | undefined``.

Outside those cases, `analyze()` runs the same inference that resources, broadcast events, model metadata, Inertia page props, and Inertia shared data publish from. What each of those features adds on top is on its own page, linked above.

`analyze()` and `ts:publish` both type the patterns these docs describe, including [a local you write more than once](./api-resources.md#local-variables-and-narrowing). Anything else, such as an expression neither can follow or a local that a reference can write, comes back `unknown` rather than a guess.

Two cases don't widen to `unknown`. [A conditional's default that can't be typed](./api-resources.md#passing-a-default-argument) leaves the value's own type in place. [A union member that can't be typed is left out](./api-resources.md#values-left-out-of-a-union), whether it's a ternary or `?:` arm or a value that a parameter's writes join. So `$cond ? $untypable : null` publishes `null`.

## Configuration Reference

`analyze()` has no config keys of its own. It follows the settings you already have, such as `enums.use_tolki_package`, `models.nullable_relations`, `timestamps_as_date`, and `custom_ts_mappings`, so its types match what `ts:publish` writes for [Enums](./enums.md), [Models](./models.md), and [API Resources](./api-resources.md). The full list is in the [Configuration Reference](./configuration-reference.md).
