spatie/typescript-transformer
Automatically generate TypeScript definitions from your PHP/Laravel code. spatie/typescript-transformer scans classes and types, then outputs .d.ts files so your frontend stays in sync with backend models, DTOs and enums with minimal manual typing.
A new option to skip manifest generation, a watch mode fix, and expanded route helper docs.
withoutManifest() option to skip manifest file generation (#153)By default the transformer writes a typescript-transformer-manifest.json file to power its caching, only rewriting output files whose contents actually changed. That manifest is unwelcome in some setups: when the output directory is a committed git submodule it surfaces as an unexpected tracked file, and when you want a clean diff or run in CI the caching simply is not needed.
TypeScriptTransformerConfigFactory now exposes a withoutManifest() method that turns off manifest generation entirely. When disabled, WriteFilesAction skips the manifest read and write and writes every file directly.
$config
->outputDirectory(resource_path('frontend/types'))
->writer(new GlobalNamespaceWriter('generated.d.ts'))
->withoutManifest();
Thanks @pawell67.
In watch mode attributes are reflected through Roave BetterReflection. PhpAttributeNode::newInstance() constructed each attribute with no arguments before invoking the result, which threw an ArgumentCountError for any attribute with required constructor arguments such as #[LiteralTypeScriptType('string[]')].
The arguments are now spread straight into the constructor, letting PHP bind positional, named, default, and variadic values itself. A regression test covers a constructor-argument attribute reflected through BetterReflection, the path that was previously untested.
Thanks @rubenvanassche.
route() throw behavior and hasRoute predicate by @rubenvanassche in https://github.com/spatie/typescript-transformer/pull/152Full Changelog: https://github.com/spatie/typescript-transformer/compare/3.2.0...3.3.0
A round of bug fixes and a couple of small extensibility improvements, plus broader generic and inherited type support.
A child class inheriting a [@var](https://github.com/var) annotation from a parent in another namespace would lose the type information. With a parent like:
// namespace App\Models
class ParentModel
{
/** [@var](https://github.com/var) string[]|SimpleGenericClass<int, string> */
public array $items;
}
A Child extends ParentModel in App\Models\Children (no use of SimpleGenericClass) used to transform $items as unknown. The transformer now resolves the annotation against the declaring class's namespace, so inherited [@var](https://github.com/var) types keep working across namespaces. Class level [@property](https://github.com/property) and constructor [@param](https://github.com/param) annotations still resolve against the current class, since they belong to that class. Thanks @ragulka.
AttributedClassTransformer extensible (#142)AttributedClassTransformer had TypeScript::class hardcoded in two places, so swapping in a custom attribute meant duplicating the entire transformer. There is now a single attributeClass() hook:
class FrontEndAttributedClassTransformer extends AttributedClassTransformer
{
protected function attributeClass(): string
{
return FrontEnd::class;
}
}
Thanks @CheshireC4t.
On macOS via Laravel Herd, PhpExecutableFinder::find() returns /Users/<me>/Library/Application Support/Herd/bin/php84. The space broke Process::fromShellCommandline("$phpBinary $command"):
sh: /Users/<me>/Library/Application: No such file or directory
The watcher then looped on Worker failed to start. Waiting for application to be fixed.... Wrapping the binary in escapeshellarg() fixes it without changing the workerCommand contract. Thanks @mdpoulter.
[@template-covariant](https://github.com/template-covariant) and [@template-contravariant](https://github.com/template-contravariant) (#144)getTemplateTagValues() defaults to the [@template](https://github.com/template) name, so generic classes annotated with the variance variants were silently losing their type parameter:
/** [@template-covariant](https://github.com/template-covariant) T */
class Paginated { /* T was dropped */ }
Both variant tag names are now collected alongside [@template](https://github.com/template). Thanks @jakewtaylor.
When FixArrayLikeStructuresClassPropertyProcessor rewrote a Collection<int, string> next to an existing string[] in a union, the output ended up as string[] | string[]. The constructor time dedup on TypeScriptUnion runs once and cannot catch duplicates introduced by later mutations. A new TypeScriptDeduplicableNode interface (implemented by TypeScriptUnion, TypeScriptIntersection, and TypeScriptArray) is now invoked by the visitor after children are visited, so any Replace or Remove that introduces duplicates is cleaned up automatically:
interface TypeScriptDeduplicableNode
{
public function deduplicateNodes(): void;
}
Fixes #137. Thanks @rubenvanassche.
A new OutputsTypeScriptLiteral trait centralizes how scalar values are written as TypeScript literals (string, int, float, bool, null). Strings are now escaped with a hand-rolled map (\, ', \n, \r, \t) and wrapped in single quotes, fixing invalid output for values like App\Models\User or it's. The trait replaces inline interpolation in TypeScriptLiteral, TypeScriptEnum, TypeScriptIdentifier, TypeScriptParameter, and TypeScriptImport, removing four duplicated quoting sites that all had the same hazard. Side effects: TypeScriptLiteral now emits single quoted strings (previously double quoted via json_encode), so the slash escaping problem from json_encode (e.g. image\/png) is gone, and float / null are now accepted by the constructor. Supersedes #138 by @pataar and #148 by @bram-pkg, both of which surfaced facets of the same bug. Thanks @rubenvanassche.
Full Changelog: https://github.com/spatie/typescript-transformer/compare/3.1.1...3.2.0
Previously, when realpath() failed on a non-existent output directory, it would silently return false, which could cause file generation to target the filesystem root (/). The transformer now validates the output directory exists and throws a clear exception if it doesn't.
Full Changelog: https://github.com/spatie/typescript-transformer/compare/3.1.0...3.1.1
[@template](https://github.com/template) generics in TypeScript output (#133)Classes with [@template](https://github.com/template) docblocks now produce generic type aliases:
/**
* [@template](https://github.com/template) T
*/
class PaginatedResponse
{
/** [@param](https://github.com/param) array<T> $data */
public function __construct(
public int $page = 1,
public array $data = [],
) {}
}
Now correctly generates:
type PaginatedResponse<T> = {
page: number;
data: T[];
};
Instead of the previous incorrect output where T was resolved as unknown.
Version 3 is a ground-up rewrite. It introduces a TypeScript AST, a visitor pattern, watch mode, a new extension system, and much more.
The package now builds a proper TypeScript Abstract Syntax Tree before writing output. Instead of generating strings directly, transformers create node objects that can be traversed and manipulated before being written to disk:
new TypeScriptAlias('User', new TypeScriptObject([
new TypeScriptProperty('name', new TypeScriptString()),
new TypeScriptProperty('age', new TypeScriptNumber()),
]));
// Output: type User = { name: string; age: number }
There are a lot of node types available and you can easily add your own!
A Visitor allows users to traverse the AST, allowing them to replace or completely remove nodes:
Visitor::create()
->after(function (TypeScriptUnion $node) {
if (count($node->types) === 1) {
return VisitorOperation::replace(array_values($node->types)[0]);
}
})
->execute($rootNode);
A file system watcher monitors your PHP files and automatically re-transforms on changes. Your TypeScript definitions stay in sync as you develop - no manual re-running required.
This feature is in beta at the moment.
TypeScriptReference nodes connect generated types to the PHP classes they represent. The system automatically resolves references to the correct import paths based on your writer configuration.
A new provider interface lets you inject custom transformed types from any source - not just PHP classes:
class AddLaravelCollectionProvider implements TransformedProvider
{
public function provide(): array
{
return [new Transformed(
typeScriptNode: new TypeScriptAlias(
new TypeScriptGeneric(new TypeScriptIdentifier('Collection'), [new TypeScriptIdentifier('T')]),
new TypeScriptGeneric(new TypeScriptIdentifier('Array'), [new TypeScriptIdentifier('T')]),
),
reference: new ClassStringReference(Collection::class),
location: ['Illuminate', 'Support'],
)];
}
}
// Output: type Collection<T> = Array<T>
Collectors have been removed. Transformers now decide both whether they can handle a type and how to transform it:
class MyTransformer extends ClassTransformer
{
protected function shouldTransform(PhpClassNode $phpClassNode): bool
{
return $phpClassNode->implementsInterface(Data::class);
}
}
The EnumTransformer now supports union output, native TypeScript enums, and a pluggable EnumProvider interface for custom enum detection.
PHPDocumentor has been replaced by PHPStan's type parser. This provides more robust handling of generics, array shapes, key-of, value-of, and complex union/intersection types.
ModuleWriter generates TypeScript modules in a directory structure mirroring your PHP namespaces. GlobalNamespaceWriter outputs a single .d.ts declaration file with namespaced types in global scope.
Transformers now work with PhpClassNode, PhpPropertyNode, PhpMethodNode instead of raw PHP Reflection objects, providing a unified interface allowing updates to the files to be handled in the same process.
DtoTransformer removed - use ClassTransformer with custom property processorsTypeProcessors replaced by ClassPropertyProcessorRecordTypeScriptType and TypeScriptTransformer attributes removedSince this is a complete rewrite, there isn't an upgrade guide available. We recommend you to first read full documentation and then upgrade your projects accordingly.
The first beta release of TypeScript Transformer v3, a complete rewrite from scratch!
I don't expect that many things will be changing between beta and release but be cautious.
Full Changelog: https://github.com/spatie/typescript-transformer/compare/2.4.0...2.5.0
nullToOptional config by @innocenzi in https://github.com/spatie/typescript-transformer/pull/88Full Changelog: https://github.com/spatie/typescript-transformer/compare/2.3.1...2.4.0
EnumTransformer by @innocenzi in https://github.com/spatie/typescript-transformer/pull/78Full Changelog: https://github.com/spatie/typescript-transformer/compare/2.3.0...2.3.1
DtoTransformer@transformPropertyName() by @cosmastech in https://github.com/spatie/typescript-transformer/pull/74Full Changelog: https://github.com/spatie/typescript-transformer/compare/2.2.2...2.3.0
Full Changelog: https://github.com/spatie/typescript-transformer/compare/2.2.1...2.2.2
npx (#29)Full Changelog: https://github.com/spatie/typescript-transformer/compare/2.1.4...2.1.5
declare keyword by default to generated output (#13)ProcessTypes to work with Collection typesHow can I help you explore Laravel packages today?