spatie / typescript-transformer Goto Github PK
View Code? Open in Web Editor NEWTransform PHP types to TypeScript
Home Page: https://docs.spatie.be/typescript-transformer/v2/introduction/
License: MIT License
Transform PHP types to TypeScript
Home Page: https://docs.spatie.be/typescript-transformer/v2/introduction/
License: MIT License
The spatie/enum
package itself and the corresponding transformer are in no way related to Laravel. So the SpatieEnumTransformer could be moved into this base package to provide this functionality more users.
Using the following versions:
"spatie/laravel-data": "^1.2.4",
"spatie/laravel-typescript-transformer": "^2.1.3",
"spatie/typescript-transformer": "^2.1.6"
The following Data object:
#[TypeScript]
class Test extends Data
{
public function __construct(
public WorkOrderType $type = WorkOrderType::UNKNOWN,
) {
}
}
enum WorkOrderType: string
{
case UNKNOWN = 'UNKNOWN;
}
Gets transformed to any:
export type Test = {
type: any;
};
Am I missing something in setup or config that causes this?
My work around is adding the following processor. Maybe this could be (improved and) included by default?
class ConvertEnumTypeProcessor implements TypeProcessor
{
public function process(
Type $type,
ReflectionParameter | ReflectionMethod | ReflectionProperty $reflection,
MissingSymbolsCollection $missingSymbolsCollection
): ?Type {
if ($type instanceof Object_ && enum_exists($type->__toString())) {
$backing_type = (new ReflectionEnum($type->__toString()))->getBackingType();
return $backing_type ? (new TypeResolver())->resolve((string) $backing_type) : $type;
}
return $type;
}
}
My Php Class
<?php
namespace Domain\Orders\DataTransferObjects;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
#[TypeScript]
class TestType
{
public string $name;
}
and i get this response in my console
+-----------+-------------------+
| PHP class | TypeScript entity |
+-----------+-------------------+
Transformed 0 PHP types to TypeScript
Although we have a concept of null in property types, we don't have a concept for nullable property types like:
name ?: string;
I can't seem to find a way to generate a type like the following:
type FleetDetails = Record<Domain.Operations.Enums.AircraftType, Domain.Operations.Data.AircraftData[]>
I had to resort to something like that:
class FleetRadarData extends Data
{
public function __construct(
#[LiteralTypeScriptType('Record<Domain.Operations.Enums.AircraftType, Domain.Operations.Data.AircraftData[]>')]
public readonly array $fleet,
// ...
) {
}
}
I don't think it's ideal because the string is not refactor-proof. I would love to be able to have an API for this using actual FQCNs, but I have no clue how I could describe the value part of the record being an array. For a simple key-value record, we could have something like this:
#[TypeScriptRecord(AircraftType::class, AircraftData::class)]
Maybe the solution would be to have a AircraftDataCollection
class but I don't know how it could be typed without adding an attribute for it and specific support.
Any input?
Right now we have to use something like this (please correct me if I'm wrong) to create array types:
#[LiteralTypeScriptType('App.Http.Resources.Email[]|null')]
public ?array $emails;
It would be great if we could reference PHP types directly and generate an array based on it.
Thanks for this great library as it worked pretty well so far for me. Though, now I face an issue when I define properties on my models:
public int $id;
public string $name;
which is correctly transformed to:
export type Team = {
id: number;
name: string;
...
}
However, PHP complains now:
Typed property App\Models\Team::$id must not be accessed before initialization
Any idea how I can avoid this error? I also tried to use default values for the properties but then again my models lose the actual database values.
If I define them like this I can avoid the PHP error but then the Typescript type is also nullable:
private ?string $name = NULL;
P.S.: I use PHP 8.0
I'm in the process of migrating a codebase to spatie/laravel-data
in conjunction with spatie/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.
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 the TypeDefinitionWriter
as well seeing as it's the default.
I played around with the idea of a custom writer that calls format
on TypeDefinitionWriter
, then implements a modified version of the logic in ModuleWriter
to append module level exports to the output, but only for types where $type->reflection->isEnum()
evaluates to true
. 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.
I use spatie/laravel-data
to trigger the bug
Example:
class Input extends BaseInput
{
/**
* @param Collection<array-key, array-key> $product_ids
*/
public function __construct(
public Collection $product_ids,
) {
}
}
php artisan typescript:transform
# ouput error
# Could not transform type: array-key
See:
If Type
implements the \phpDocumentor\Reflection\PseudoType
interface, it should call \phpDocumentor\Reflection\PseudoType::underlyingType()
to return the correct Type
I really hope I'm not blind here and I missed that warning but I feel like it'd be useful to specify somewhere in the docs that the order in which you specify which Transformers to use matters.
DtoTransformer
before EnumTransformer
$config = TypeScriptTransformerConfig::create()
->autoDiscoverTypes(__DIR__ . '/../Contracts')
->transformToNativeEnums()
->transformers([
DtoTransformer::class,
EnumTransformer::class,
])
->outputFile(__DIR__ . '/../../assets/generated/types.d.ts')
;
The above PHP configuration results in DtoTransformer
handling everything, including enums which then results in the wrong TypeScript being generated.
declare namespace App.Contracts {
export type MapDefinition = {
layers: { [key: string]: any };
projection: [string, string];
centerCoordinates: [number, number];
};
export type NodeType = {
name: string;
value: number;
};
}
EnumTransformer
before DtoTransformer
$config = TypeScriptTransformerConfig::create()
->autoDiscoverTypes(__DIR__ . '/../Contracts')
->transformToNativeEnums()
->transformers([
EnumTransformer::class,
DtoTransformer::class,
])
->outputFile(__DIR__ . '/../../assets/generated/types.d.ts')
;
The above configuration correctly lets the EnumTransformer
act first and generate a correct TypeScript enum.
declare namespace App.Contracts {
export enum NodeType { 'OutdoorPath' = 0, 'Hallway' = 1, 'Room' = 2, 'Stairs' = 3, 'Elevator' = 4 };
export type MapDefinition = {
layers: { [key: string]: any };
projection: [string, string];
centerCoordinates: [number, number];
};
}
In retrospect, it makes complete sense that the order of transformers matters but it wasn't until I ran into this problem that I figured that out. I don't think the behavior should change but the docs should have a warning talking about that fact. I'd send a PR but I'm not sure where to best place that information.
But thank you for this library! It's been such a timesaver for my project! ❤️
Having some issues using the PHP 8 attributes.
Using the comment based /** @typescript */ declaration works fine however i need to be able to use the other attributes like: #[LiteralTypeScriptType("string | null")]
Ive confirmed that php 8.3.8 is running.
Running in laravel 11.
Any ideas why this might be ?
I was wondering if there was a mechanism to include multiple search paths? We're looking to introduce to a fairly large code base and it can take a long time to search through all the classes. However, we have classes in a couple different top level directories that we'd like to convert.
app/
Foo/
Bar/
Irrelevant/
Something like search_paths' => [app_path('Foo'), app_path('Bar')]
would be awesome but I'm not sure how to go about it.
Hello, there!
As part of the university research we are currently doing regarding the security of Github Actions, we noticed that one or many of the workflows that are part of this repository are referencing vulnerable versions of the third-party actions. As part of a disclosure process, we decided to open issues to notify GitHub Community.
Please note that there are could be some false positives in our methodology, thus not all of the open issues could be valid. If that is the case, please let us know, so that we can improve on our approach. You can contact me directly using an email: ikoishy [at] ncsu.edu
Thanks in advance
The vulnerability fix that is missing by actions' versions could be related to:
(1) CVE fix
(2) upgrade of vulnerable dependency
(3) fix to secret leak and others.
Please consider updating the reference to the action.
If you end up updating the reference, please let us know. We need the stats for the paper :-)
install command on a new Laravel project:
composer require spatie/laravel-typescript-transformer
Generates this issue:
Problem 1
- spatie/typescript-transformer[2.1.13, ..., 2.2.2] require nikic/php-parser ^4.13 -> found nikic/php-parser[v4.13.0, ..., v4.18.0] but the package is fixed to v5.0.0 (lock file version) by a partial update and that version does not match.
- spatie/laravel-typescript-transformer[2.3.0, ..., 2.3.2] require spatie/typescript-transformer ^2.1.13 -> satisfiable by spatie/typescript-transformer[2.1.13, ..., 2.2.2].
Temporary workaround is to first install an older php-parser
version by adding this to your require
section of composer.json
:
"nikic/php-parser": "^4.1"
PHP Enums and backed Enums are not correctly converted to TS Enums
#14 introduced TS enums transformations but the toEnum method always create string enums
Is this the intended way ?
I am just getting started so I tried the code from https://spatie.be/docs/typescript-transformer/v2/usage/getting-started. When running it, it seems there is a vital Composer package (myclabs/php-enum
) missing for the transformer to run.
Here is my script:
<?php
use Spatie\TypeScriptTransformer\Transformers\MyclabsEnumTransformer;
use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig;
use Spatie\TypeScriptTransformer\TypeScriptTransformer;
$config = TypeScriptTransformerConfig::create()
// path where your PHP classes are
->autoDiscoverTypes(__DIR__ . '/../src')
// list of transformers
->transformers([MyclabsEnumTransformer::class])
// file where TypeScript type definitions will be written
->outputFile(__DIR__ . '/generated.d.ts')
;
TypeScriptTransformer::create($config)->transform();
When running it, it throws the following error on the console:
In MyclabsEnumTransformer.php line 18:
Class "MyCLabs\Enum\Enum" does not exist
Installing myclabs/php-enum
via Composer fixed the issue. Did I oversaw something important? And yes, I marked at least one class using @typescript
.
So, in TypeScript we use null
very sparsely. Usually we use undefined
in interfaces.
The difference is confusing because, well, it's JavaScript -- but basically null
is when you want to have to explicitly declare a variable with no value, whereas undefined
is for cases when a variable should not necessarily be initialized.
Aside from the theory, the practical difference is that you may omit declaring undefined
union variables/properties, but you can't omit null
union variables/properties, you have to declare them.
For instance, the following would be valid:
interface User {
id?: number
name: string
}
const user: User = {
name: 'John'
}
While the following is not:
interface User {
id: number | null
name: string
}
const user: User = {
name: 'John'
// Error! `name` is missing
}
For this reason, I think that it would be better to convert nullable PHP types to use undefined
instead of an explicit null
.
I'm willing to PR but I didn't want to source-dive if you weren't going to agree with that opinion.
Hello,
i'm just trying your package and since now it looks really impressive
but now i have the following situation:
i have php-models, that have private properties BUT they are "accessible from public" (a small implementation with magic getters to only get the data loaded if needed)
is there a way (like [#Hidden] but the other way ^^) to tell your script to include them other then defining the whole typescript myself?
Currently, when creating typescript definitions of spatie/laravel-data objects, it's required to add an docblock static the arrayable type of the property:
#[TypeScript]
class UserData extends Data {
public function __construct(
public string $name,
/** @var PostData[] $posts */
#[DataCollectionOf(PostData::class)]
public DataCollection $posts,
) {}
}
It would be great if it was possible to remove the docblock there and infer the type from the DataCollectionOf
attribute.
In most cases transforming PHP backed enums to type is preferable. However in some cases we need it as a Typescript Enum
, for instance when we need to iterate it in order to generate a list of options. Currently it is either types or Enums, configurable using the TypeScriptTransformerConfig
.
It would be very useful if the Enum and the Type transforming can be split in 2 Transformer classes, e.g. TypeEnumTransformer
and NativeEnumTransformer
. The EnumTransformer
uses these and additionally you can use the Attribute #[TypeScriptTransformer(NativeEnumTransformer::class)]
when you need a Typescript native enum.
Does the above make sense and are you interested in a PR?
phpdocs with arrayshapes fails.
for exemple: /** @var array{a: int[], b: string[], c: float} $config **/
first it fails at the regexp:
then it fails at transpiling (phpDocumentor\Reflection\PseudoTypes\ArrayShape)
it's partly solved with:
Senario 1
Input: /** @var array<string, array{a: int[], b: string[], c: float}> $config **/
Expected: {[key: string]: {a: number[], b: string[], c: number}}
Current: RuntimeException Unexpected token "", expected '>' at offset 19 on line 1
,
due to the regexp only catching @var array<string, array
I've got the following situation. I want to map the enum values to a class-string so I can use them for my polymorphic relation and in the modal to cast them to a human readable enum.
<?php
namespace App\Enums\Pokemon;
use App\Models\Pokemon\PokemonEncounter;
use App\Models\Pokemon\PokemonEncounterMove;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
#[TypeScript()]
enum BattleRoundActionType: string
{
case SwapPokemon = PokemonEncounter::class;
case ExecuteMove = PokemonEncounterMove::class;
}
Then later in the model, I can do the following:
<?php
namespace App\Models\Pokemon;
use App\Enums\Pokemon\BattleRoundActionType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class BattleRound extends Model
{
protected $casts = [
'initiator_action_id' => 'int',
'initiator_action_type' => BattleRoundActionType::class,
];
public function initiatorAction(): MorphTo
{
return $this->morphTo();
}
}
However, when running the TypeScript generator, this results in the following code:
declare namespace App.Enums.Pokemon {
export type BattleRoundActionType =
| 'AppModelsPokemonPokemonEncounter'
| 'AppModelsPokemonPokemonEncounterMove';
}
This is what I expected:
declare namespace App.Enums.Pokemon {
export type BattleRoundActionType =
| 'App\\Models\\Pokemon\\PokemonEncounter';
| 'App\\Models\\Pokemon\\PokemonEncounterMove';
}
Running:
I found out that it is Prettier that removes the backslashes because they have no use. Without Prettier, the result looks like this:
declare namespace App.Enums.Pokemon {
export type BattleRoundActionType = 'App\Models\Pokemon\PokemonEncounter' | 'App\Models\Pokemon\PokemonEncounterMove';
}
Still not what I expected, the backslashes should be double, but it explains why there were no backslashes at all.
I think it would be great to be able to use the transformer without artisan, e.g. in non-laravel apps or in laravel packages that don't have artisan installed.
Maybe this could be achieved by providing a more general CLI as a vendor binary? The path to the config file could be provided as an argument.
If you are open to the idea, I'd be happy to give it a shot and create a pull request.
Thanks,
Michael
I'm running the script on windows 11 with PHP version 8.2.7 I've also tried several approaches without success:
set up a project with only "spatie/typescript-transformer" v2 installed... 0 TypeScript code generated.
create a Laravel project and publish the configuration of "spatie/laravel-typescript-transformer" v2... 0 TypeScript code generated.
+-----------+-------------------+
| PHP class | TypeScript entity |
+-----------+-------------------+
Transformed 0 PHP types to TypeScript
I've tested both approaches to commenting my PHP classes.
/** @typescript */
and
#[TypeScript]
I'm a little desperate
my composer.json
{
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.1",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8",
"spatie/laravel-typescript-transformer": "^2.3"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.1",
"spatie/laravel-ignition": "^2.0"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
my typescript-transformer.php
<?php
/*
* Copyright (c) 2023 La Maison Fantastique - All Rights Reserved
*/
declare(strict_types=1);
use Spatie\LaravelTypeScriptTransformer\Transformers\DtoTransformer;
use Spatie\LaravelTypeScriptTransformer\Transformers\SpatieStateTransformer;
use Spatie\TypeScriptTransformer\Collectors\DefaultCollector;
use Spatie\TypeScriptTransformer\Collectors\EnumCollector;
use Spatie\TypeScriptTransformer\Formatters\PrettierFormatter;
use Spatie\TypeScriptTransformer\Transformers\EnumTransformer;
use Spatie\TypeScriptTransformer\Transformers\InterfaceTransformer;
use Spatie\TypeScriptTransformer\Transformers\SpatieEnumTransformer;
return [
/*
* The paths where typescript-transformer will look for PHP classes
* to transform, this will be the `app` path by default.
*/
'auto_discover_types' => [app_path('/../../../www/app')],
/*
* Collectors will search for classes in the `auto_discover_types` paths and choose the correct
* transformer to transform them. By default, we include a DefaultCollector which will search
* for @typescript annotated and ![TypeScript] attributed classes to transform.
*/
'collectors' => [
DefaultCollector::class,
EnumCollector::class,
],
/*
* Transformers take PHP classes(e.g., enums) as an input and will output
* a TypeScript representation of the PHP class.
*/
'transformers' => [
DtoTransformer::class,
EnumTransformer::class,
InterfaceTransformer::class,
SpatieEnumTransformer::class,
SpatieStateTransformer::class,
],
/*
* In your classes, you sometimes have types that should always be replaced
* by the same TypeScript representations. For example, you can replace a
* Datetime always with a string. You define these replacements here.
*/
'default_type_replacements' => [
// DateTime::class => 'string',
// DateTimeImmutable::class => 'string',
// Carbon\CarbonImmutable::class => 'string',
// Carbon\Carbon::class => 'string',
],
// The package will write the generated TypeScript to this file.
'output_file' => resource_path('/../../../www/src/@types/generated.d.ts'),
/*
* When the package is writing types to the output file, a writer is used to
* determine the format. By default, this is the `TypeDefinitionWriter`.
* But you can also use the `ModuleWriter` or implement your own.
*/
'writer' => Spatie\TypeScriptTransformer\Writers\ModuleWriter::class,
/*
* The generated TypeScript file can be formatted. We ship a Prettier formatter
* out of the box: `PrettierFormatter` but you can also implement your own one.
* The generated TypeScript will not be formatted when no formatter was set.
*/
'formatter' => PrettierFormatter::class,
/*
* Enums can be transformed into types or native TypeScript enums, by default
* the package will transform them to types.
*/
'transform_to_native_enums' => false,
];
I deliberately targeted a project outside the scope of Laravel because I don't use it in my project and I didn't want to pollute it with a Laravel installation.
Hi Spatie! Thanks for amazing package.
I have some issue with generated ts file - our fe guys wants all entities be w\o namespaces.
Something like:
typescript-transformer.php
...
'preserve_namespaces' => true/false,
.....
Please add this feature
It would be nice to be able to configure the fallback values for unknown symbols and mixed.
Currently unknown symbols/classes is always converted to any
.
I'd love to be able to configure that value to be either unknown
or maybe even {[key: string]: unknown}
as we know it's a class/object.
docs/usage/using-transformers.md has a link at the very bottom that should lead to docs/transformers/getting-started.md, however it's broken and redirects to docs/introduction.md.
typescript-transformer/docs/usage/using-transformers.md
Lines 62 to 64 in 00d21fa
The link in the .md source file seems to be structured correctly:
Upon further inspection in the browser, it appears that the link is not resolved to an absolute url, but to the relative url just like in the document itself.
<!-- Link expected to resolve to this -->
<a href="https://spatie.be/docs/typescript-transformer/v2/transformers/getting-started">writing transformers</a>
<!-- Instead we get this -->
<a href="docs/typescript-transformer/v2/transformers/getting-started">writing transformers</a>
The issue manifests because the source file and the target file are located in different namespaces (directories), clicking on the link appends it to the current namespace, eventually resolving to https://spatie.be/docs/typescript-transformer/v2/usage/docs/typescript-transformer/v2/transformers/getting-started
, a link that doesn't exist, hence redirection to the introduction page.
This issue can be fixed in one of two ways.
We've added a whole section in the docs about [writing transformers](https://spatie.be/docs/typescript-transformer/v2/transformers/getting-started).
We've added a whole section in the docs about [writing transformers](../transformers/getting-started).
The second mitigation is cleaner in my opinion, a PR with such a mitigation will be submitted momentarily.
This might be the only instance of this issue in spatie/typescript-transformer
docs, but as far as I know, all spatie packages generate their docs the same way, therefore similar issues might be present in docs of other packages.
I suggest to use a script to scan the docs directory in every spatie package to catch any instance of a file having a link to another file in a different namespace.
Also, having a Github Actions job that warns/fixes this issue will insure future proofing against introducing such links accidentally.
is there any time window you expect to be V3 finished?
thanks for answer
In Laravel 9.48 I've composer required
Laravel Data and then this typescript-transformer package.
When I run php artisan typescript:transform
it complaints about several missing dependencies, related to nesbot/carbon package:
Interface "PHPStan\Reflection\MethodsClassReflectionExtension" not found at vendor/nesbot/carbon/src/Carbon/PHPStan/MacroExtension.php:27
Class "Doctrine\DBAL\Types\VarDateTimeType" not found at vendor/nesbot/carbon/src/Carbon/Doctrine/DateTimeType.php:12
I have mostly been doing node/NPM and my expectation would be that these dependencies were handled automatically with composer, like in npm, but maybe I'm wrong.
I apologize if this is not really an issue with typescript-transformer.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.