Closed felixpackard closed 1 month ago
Hi.
I'm tackling the same problem.
I created a custom transform command and two custom writers.
The OnlyEnumsModuleWriter
writer is responsible to only export enums and the ExceptEnumsModuleWriter
writer is responsible to export non-enums types and to import the enums types properly.
I didn't check the $type->reflection->isEnum()
but the enum
keywork to support non-native enums.
I added a enum_output_file
entry to the typescript-transformer
config:
/*
* The package will write the generated TypeScript to this file.
*/
'output_file' => resource_path('js/types/generated.d.ts'),
'enum_output_file' => resource_path('js/enums.generated.ts'),
Here is the composer script to export types:
{
"scripts": {
"ts": [
"@php artisan typescript:custom-transform"
],
}
}
The custom transform command, it should be registered in the console Kernel
or in the bootstrap/app.php
file:
<?php
namespace App\Domain\Support\TypeScriptTransformer;
use Illuminate\Console\Command;
use Spatie\TypeScriptTransformer\Formatters\PrettierFormatter;
use Spatie\TypeScriptTransformer\Structures\TransformedType;
use Spatie\TypeScriptTransformer\TypeScriptTransformer;
use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig;
class CustomTransformCommand extends Command
{
protected $signature = 'typescript:custom-transform
{--format : Use Prettier to format the output}';
protected $description = 'Map PHP types to TypeScript, separating enums and non-enums into different files.';
public function handle(TypeScriptTransformerConfig $config): int
{
if ($this->option('format')) {
$config->formatter(PrettierFormatter::class);
}
// Export non-enums first
$transformer = new TypeScriptTransformer(
$config
->outputFile(config('typescript-transformer.output_file')) // @phpstan-ignore-line
->writer(ExceptEnumsModuleWriter::class)
);
$transformer->transform();
// Then export only enums
$config
->outputFile(config('typescript-transformer.enum_output_file')) // @phpstan-ignore-line
->writer(OnlyEnumsModuleWriter::class);
$transformer = new TypeScriptTransformer($config);
/** @var \Illuminate\Support\Collection<string, \Spatie\TypeScriptTransformer\Structures\TransformedType> $collection */
$collection = collect($transformer->transform());
$this->table(
['PHP class', 'TypeScript entity'],
$collection->map(fn (TransformedType $type, string $class) => [
$class, $type->getTypeScriptName(),
])
);
$this->info("Transformed {$collection->count()} PHP types to TypeScript");
return 0;
}
}
The OnlyEnumsWriter:
<?php
namespace App\Domain\Support\TypeScriptTransformer;
use Spatie\TypeScriptTransformer\Structures\TransformedType;
use Spatie\TypeScriptTransformer\Structures\TypesCollection;
use Spatie\TypeScriptTransformer\Writers\Writer;
class OnlyEnumsModuleWriter implements Writer
{
public function format(TypesCollection $collection): string
{
$output = '';
/** @var \ArrayIterator $iterator */
$iterator = $collection->getIterator();
$iterator->uasort(function (TransformedType $a, TransformedType $b) {
return strcmp($a->name, $b->name);
});
foreach ($iterator as $type) {
/** @var \Spatie\TypeScriptTransformer\Structures\TransformedType $type */
if ($type->isInline) {
continue;
}
if ($type->keyword !== 'enum') {
continue;
}
$output .= "export {$type->toString()}".PHP_EOL;
}
return $output;
}
public function replacesSymbolsWithFullyQualifiedIdentifiers(): bool
{
return false;
}
}
The ExceptEnumsWriter:
<?php
namespace App\Domain\Support\TypeScriptTransformer;
use Exception;
use Spatie\TypeScriptTransformer\Structures\TransformedType;
use Spatie\TypeScriptTransformer\Structures\TypesCollection;
use Spatie\TypeScriptTransformer\Writers\Writer;
class ExceptEnumsModuleWriter implements Writer
{
public function format(TypesCollection $collection): string
{
$output = '';
$iterator = $collection->getIterator();
$iterator->uasort(function (TransformedType $a, TransformedType $b) {
return strcmp($a->name, $b->name);
});
$enums = [];
foreach ($iterator as $type) {
/** @var \Spatie\TypeScriptTransformer\Structures\TransformedType $type */
if ($type->isInline) {
continue;
}
if ($type->keyword === 'enum') {
$enums[] = $type->name;
continue;
}
$output .= "export {$type->toString()}".PHP_EOL;
}
if ($enums) {
$enumsImportOutput = 'import { '.implode(', ', $enums).' } from "'.$this->relativePathToEnumsDefinition().'";'.PHP_EOL.PHP_EOL;
}
return ($enumsImportOutput ?? '').$output;
}
public function replacesSymbolsWithFullyQualifiedIdentifiers(): bool
{
return false;
}
private function relativePathToEnumsDefinition(): string
{
$from = config('typescript-transformer.output_file');
$to = config('typescript-transformer.enum_output_file');
if (! is_string($from) || ! is_string($to)) {
throw new Exception('Cannot resolve relative path between configurations [typescript-transformer.output_file] and [typescript-transformer.enum_output_file].');
}
$from = str_replace('\\', '/', $from);
$to = str_replace('\\', '/', $to);
if ($from == $to) {
throw new Exception('The following path should be different: [typescript-transformer.output_file] and [typescript-transformer.enum_output_file].');
}
// Split the paths into arrays
$fromParts = explode('/', $from);
$toParts = explode('/', $to);
// Find the point where the paths diverge
$i = 0;
while (isset($fromParts[$i], $toParts[$i]) && $fromParts[$i] === $toParts[$i]) {
$i++;
}
// Go up as many levels as necessary
$up = array_fill(0, count($fromParts) - $i - 1, '..');
// Descend into the destination path
$down = array_slice($toParts, $i);
// Build the relative path
$relativePath = implode('/', array_merge($up, $down));
// If the relative path does not start with "..", prepend "./"
if (! str_starts_with($relativePath, '..')) {
$relativePath = "./{$relativePath}";
}
return $relativePath;
}
}
@shaffe-fr Thanks for sharing your approach! I ended up going with a similar approach of creating a custom transform command and a custom writer, but I love the attention to detail in your solution, which definitely improves on what I'm using currently, so I'll probably steal parts of it.
I didn't replace the default writer, so the enums are duplicated – which is convenient if I'm just typing data from the backend, but it's easy to forget to import them from the module when using them as a value. I also was lazy and hard-coded values like the output path for the enums, which isn't awful but probably not great practice.
Also facing this problem thankyou for your solution!
I've also been struggling with this, I use the TypeDefinitionWriter
and not the ModuleWriter
because I need the namespaces.
My solution was to just make my own new writer that extends the base one and instead of declare namespace
here: https://github.com/spatie/typescript-transformer/blob/4e1b7a14ae8ac9a548f776af4f492e4d569e3d38/src/Writers/TypeDefinitionWriter.php#L22 I just do an export namespace
instead.
It lets me do this in my typescript for example:
import { App } from '@/types/backend';
import MeasurementType = App.Enums.MeasurementType;
if (measurementType === MeasurementType.HECTARES) {
// ...
}
Not sure if it's the best thing to do but working for me so far so hopefully it's helpful to others 🤙
Good solutions, I'm going to close this since there are enough solutions for v2. In v3 a writer will have the following signature:
/** @return array<WriteableFile> */
public function output(
TransformedCollection $collection,
): array;
Allowing you to split the TransformedCollection between different writers and outputting multiple files at once so then implementing a custom writer should be a lot easier!
I'm in the process of migrating a codebase to
spatie/laravel-data
in conjunction withspatie/typescript-transformer
so we don't have to manually duplicate PHP types and enums (int-backed) in TypeScript.One issue that I've run into is since all generated types are encapsulated by
declare namespace
blocks, enums generated by this package aren't able to be used as runtime values – e.g. declaring a default sorting method within the application state – they can only be used for type checking.Is there any way this package could, for example, output enums to a regular
.ts
file in addition to the.d.ts
file, or is there another solution to this that I'm missing?I'm happy to submit a PR for this if there's an easy fix.
Edit 1 – ModuleWriter
I realise the
ModuleWriter
may be a solution to this, but it would be nice if there was a way to solve this when using theTypeDefinitionWriter
as well seeing as it's the default.Edit 2 – Custom writer
I played around with the idea of a custom writer that calls
format
onTypeDefinitionWriter
, then implements a modified version of the logic inModuleWriter
to append module level exports to the output, but only for types where$type->reflection->isEnum()
evaluates totrue
. Turns out as soon as you include a module level export in the file, the namespaced types are no longer accessible, so the module exports would in fact have to be in a separate file.