diff --git a/src/Actions/TranspileTypeToTypeScriptAction.php b/src/Actions/TranspileTypeToTypeScriptAction.php index c31f6df..448e6f0 100644 --- a/src/Actions/TranspileTypeToTypeScriptAction.php +++ b/src/Actions/TranspileTypeToTypeScriptAction.php @@ -32,11 +32,15 @@ class TranspileTypeToTypeScriptAction private ?string $currentClass; + private bool $nullablesAreOptional; + public function __construct( MissingSymbolsCollection $missingSymbolsCollection, + bool $nullablesAreOptional = false, ?string $currentClass = null ) { $this->missingSymbolsCollection = $missingSymbolsCollection; + $this->nullablesAreOptional = $nullablesAreOptional; $this->currentClass = $currentClass; } @@ -84,6 +88,10 @@ private function resolveListType(AbstractList $list): string private function resolveNullableType(Nullable $nullable): string { + if ($this->nullablesAreOptional) { + return $this->execute($nullable->getActualType()); + } + return "{$this->execute($nullable->getActualType())} | null"; } diff --git a/src/Collectors/DefaultCollector.php b/src/Collectors/DefaultCollector.php index 4626151..5fcb2a1 100644 --- a/src/Collectors/DefaultCollector.php +++ b/src/Collectors/DefaultCollector.php @@ -37,9 +37,11 @@ protected function resolveAlreadyTransformedType(ClassTypeReflector $reflector): { $missingSymbols = new MissingSymbolsCollection(); $name = $reflector->getName(); + $nullablesAreOptional = $this->config->shouldConsiderNullAsOptional(); $transpiler = new TranspileTypeToTypeScriptAction( $missingSymbols, + $nullablesAreOptional, $name ); diff --git a/src/Transformers/DtoTransformer.php b/src/Transformers/DtoTransformer.php index 9ec1e07..a39fbde 100644 --- a/src/Transformers/DtoTransformer.php +++ b/src/Transformers/DtoTransformer.php @@ -54,22 +54,26 @@ protected function transformProperties( ReflectionClass $class, MissingSymbolsCollection $missingSymbols ): string { - $isOptional = ! empty($class->getAttributes(Optional::class)); + $isClassOptional = ! empty($class->getAttributes(Optional::class)); + $nullablesAreOptional = $this->config->shouldConsiderNullAsOptional(); return array_reduce( $this->resolveProperties($class), - function (string $carry, ReflectionProperty $property) use ($isOptional, $missingSymbols) { + function (string $carry, ReflectionProperty $property) use ($isClassOptional, $missingSymbols, $nullablesAreOptional) { $isHidden = ! empty($property->getAttributes(Hidden::class)); if ($isHidden) { return $carry; } - $isOptional = $isOptional || ! empty($property->getAttributes(Optional::class)); + $isOptional = $isClassOptional + || ! empty($property->getAttributes(Optional::class)) + || ($property->getType()?->allowsNull() && $nullablesAreOptional); $transformed = $this->reflectionToTypeScript( $property, $missingSymbols, + $isOptional, ...$this->typeProcessors() ); diff --git a/src/Transformers/InterfaceTransformer.php b/src/Transformers/InterfaceTransformer.php index 9e2303d..c406cf6 100644 --- a/src/Transformers/InterfaceTransformer.php +++ b/src/Transformers/InterfaceTransformer.php @@ -35,6 +35,7 @@ function (string $parameterCarry, \ReflectionParameter $parameter) use ($missing $type = $this->reflectionToTypeScript( $parameter, $missingSymbols, + false, ...$this->typeProcessors() ); @@ -53,6 +54,7 @@ function (string $parameterCarry, \ReflectionParameter $parameter) use ($missing $returnType = $this->reflectionToTypeScript( $method, $missingSymbols, + false, ...$this->typeProcessors() ); } diff --git a/src/Transformers/TransformsTypes.php b/src/Transformers/TransformsTypes.php index 6863dae..a9abeb8 100644 --- a/src/Transformers/TransformsTypes.php +++ b/src/Transformers/TransformsTypes.php @@ -16,6 +16,7 @@ trait TransformsTypes protected function reflectionToTypeScript( ReflectionMethod | ReflectionProperty | ReflectionParameter $reflection, MissingSymbolsCollection $missingSymbolsCollection, + bool $nullablesAreOptional = false, TypeProcessor ...$typeProcessors ): ?string { $type = $this->reflectionToType( @@ -31,6 +32,7 @@ protected function reflectionToTypeScript( return $this->typeToTypeScript( $type, $missingSymbolsCollection, + $nullablesAreOptional, $reflection->getDeclaringClass()?->getName() ); } @@ -60,10 +62,12 @@ protected function reflectionToType( protected function typeToTypeScript( Type $type, MissingSymbolsCollection $missingSymbolsCollection, + bool $nullablesAreOptional = false, ?string $currentClass = null, ): string { $transpiler = new TranspileTypeToTypeScriptAction( $missingSymbolsCollection, + $nullablesAreOptional, $currentClass, ); diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index 0d0c763..68ec600 100644 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -29,6 +29,8 @@ class TypeScriptTransformerConfig private bool $transformToNativeEnums = false; + private bool $nullToOptional = false; + public static function create(): self { return new self(); @@ -90,6 +92,13 @@ public function transformToNativeEnums(bool $transformToNativeEnums = true): sel return $this; } + public function nullToOptional(bool $nullToOptional = false): self + { + $this->nullToOptional = $nullToOptional; + + return $this; + } + public function getAutoDiscoverTypesPaths(): array { return $this->autoDiscoverTypesPaths; @@ -162,4 +171,9 @@ public function shouldTransformToNativeEnums(): bool { return $this->transformToNativeEnums; } + + public function shouldConsiderNullAsOptional(): bool + { + return $this->nullToOptional; + } } diff --git a/tests/Actions/TranspileTypeToTypeScriptActionTest.php b/tests/Actions/TranspileTypeToTypeScriptActionTest.php index da0d6c6..87fcb28 100644 --- a/tests/Actions/TranspileTypeToTypeScriptActionTest.php +++ b/tests/Actions/TranspileTypeToTypeScriptActionTest.php @@ -4,14 +4,15 @@ use phpDocumentor\Reflection\Types\Self_; use phpDocumentor\Reflection\Types\Static_; use phpDocumentor\Reflection\Types\This; -use function PHPUnit\Framework\assertContains; -use function PHPUnit\Framework\assertEquals; -use function Spatie\Snapshots\assertMatchesSnapshot; use Spatie\TypeScriptTransformer\Actions\TranspileTypeToTypeScriptAction; use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; use Spatie\TypeScriptTransformer\Tests\FakeClasses\Enum\RegularEnum; use Spatie\TypeScriptTransformer\Types\StructType; +use function PHPUnit\Framework\assertContains; +use function PHPUnit\Framework\assertEquals; +use function Spatie\Snapshots\assertMatchesSnapshot; + beforeEach(function () { $this->missingSymbols = new MissingSymbolsCollection(); @@ -19,6 +20,7 @@ $this->action = new TranspileTypeToTypeScriptAction( $this->missingSymbols, + false, 'fake_class' ); }); @@ -62,3 +64,17 @@ expect($transformed)->toBe('string | number'); }); + +it('does not add nullable unions to optional properties', function () { + $action = new TranspileTypeToTypeScriptAction( + $this->missingSymbols, + true + ); + + $transformed = $action->execute(StructType::fromArray([ + 'a_string' => 'string', + 'a_nullable_string' => '?string', + ])); + + assertEquals('{a_string:string;a_nullable_string:string;}', $transformed); +}); diff --git a/tests/Transformers/DtoTransformerTest.php b/tests/Transformers/DtoTransformerTest.php index cc97add..0c4b533 100644 --- a/tests/Transformers/DtoTransformerTest.php +++ b/tests/Transformers/DtoTransformerTest.php @@ -2,9 +2,6 @@ use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Types\String_; -use function PHPUnit\Framework\assertEquals; -use function Spatie\Snapshots\assertMatchesSnapshot; -use function Spatie\Snapshots\assertMatchesTextSnapshot; use Spatie\TypeScriptTransformer\Attributes\Hidden; use Spatie\TypeScriptTransformer\Attributes\LiteralTypeScriptType; use Spatie\TypeScriptTransformer\Attributes\Optional; @@ -20,6 +17,10 @@ use Spatie\TypeScriptTransformer\TypeProcessors\TypeProcessor; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; +use function PHPUnit\Framework\assertEquals; +use function Spatie\Snapshots\assertMatchesSnapshot; +use function Spatie\Snapshots\assertMatchesTextSnapshot; + beforeEach(function () { $config = TypeScriptTransformerConfig::create() ->defaultTypeReplacements([ @@ -48,10 +49,10 @@ it('a type processor can remove properties', function () { $config = TypeScriptTransformerConfig::create(); - $transformer = new class($config) extends DtoTransformer { + $transformer = new class ($config) extends DtoTransformer { protected function typeProcessors(): array { - $onlyStringPropertiesProcessor = new class implements TypeProcessor { + $onlyStringPropertiesProcessor = new class () implements TypeProcessor { public function process( Type $type, ReflectionProperty | ReflectionParameter | ReflectionMethod $reflection, @@ -74,7 +75,7 @@ public function process( }); it('will take transform as typescript attributes into account', function () { - $class = new class { + $class = new class () { #[TypeScriptType('int')] public $int; @@ -102,7 +103,7 @@ public function process( }); it('transforms properties to optional ones when using optional attribute', function () { - $class = new class { + $class = new class () { #[Optional] public string $string; }; @@ -133,7 +134,7 @@ class DummyOptionalDto it('transforms properties to hidden ones when using hidden attribute', function () { - $class = new class() { + $class = new class () { public string $visible; #[Hidden] public string $hidden; @@ -146,3 +147,17 @@ class DummyOptionalDto assertMatchesSnapshot($type->transformed); }); + +it('transforms nullable properties to optional ones according to config', function () { + $class = new class () { + public ?string $string; + }; + + $config = TypeScriptTransformerConfig::create()->nullToOptional(true); + $type = (new DtoTransformer($config))->transform( + new ReflectionClass($class), + 'Typed' + ); + + $this->assertMatchesSnapshot($type->transformed); +}); diff --git a/tests/Transformers/InterfaceTransformerTest.php b/tests/Transformers/InterfaceTransformerTest.php index 784599d..df7f2b7 100644 --- a/tests/Transformers/InterfaceTransformerTest.php +++ b/tests/Transformers/InterfaceTransformerTest.php @@ -1,12 +1,13 @@