Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions docs/2-features/18-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,33 @@ This command scans your marked classes, generates the corresponding TypeScript d

Tempest provides several built-in type resolvers for common types: strings, numbers, dates, enums and class references.

You can add your own resolver by providing implementations of {b`Tempest\Generation\TypeScript\TypeResolvers\TypeResolver`}. This interface requires a `canResolve()` method to determine if the resolver can handle a given type, and a `resolve()` method to perform the actual resolution.
You can add your own resolver by implementing {b`Tempest\Generation\TypeScript\TypeResolver`}. This interface requires a `canResolve()` method to determine if the resolver can handle a given type, and a `resolve()` method that returns a type node.

The following is the actual implementation of the built-in resolver that handles scalar types:
The following is the actual implementation of the built-in resolver that handles enum cases:

```php ScalarTypeResolver.php
```php EnumCaseTypeResolver.php
#[Priority(Priority::LOW)]
final class ScalarTypeResolver implements TypeResolver
final class EnumCaseTypeResolver implements TypeResolver
{
public function canResolve(TypeReflector $type): bool
{
return $type->isBuiltIn()
&& in_array($type->getName(), ['string', 'int', 'float', 'bool'], strict: true);
return $type->isEnumCase();
}

public function resolve(TypeReflector $type, TypeScriptGenerator $generator): ResolvedType
public function resolve(TypeReflector $type, TypeScriptGenerator $generator): TypeNode
{
return new ResolvedType(match ($type->getName()) {
'string' => 'string',
'int', 'float' => 'number',
'bool' => 'boolean',
});
$case = $type->asEnumCase()->getValue();
$value = $case instanceof BackedEnum
? $case->value
: $case->name;

return new LiteralTypeNode($value);
}
}
```

Resolvers may return any supported semantic node depending on your use case, such as {b`Tempest\Generation\TypeScript\TypeNodes\PrimitiveTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\LiteralTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\SymbolTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\ArrayTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\UnionTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\IntersectionTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\ObjectTypeNode`} or {b`Tempest\Generation\TypeScript\TypeNodes\RawTypeNode`}.

:::info
Type resolvers are automatically [discovered](../1-essentials/05-discovery.md) and do not need to be registered manually.
:::
Expand Down
9 changes: 5 additions & 4 deletions packages/generation/src/TypeScript/PropertyDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@

namespace Tempest\Generation\TypeScript;

use Tempest\Generation\TypeScript\TypeNodes\TypeNode;

/**
* Represents a property in a TypeScript interface.
*/
final readonly class PropertyDefinition
{
/**
* @param string $name The name of the property.
* @param string $definition The TypeScript definition of the property.
* @param null|string $fqcn The PHP FQCN of the original type.
* @param TypeNode $type The TypeScript type of the property.
* @param bool $isNullable Whether the property is nullable.
*/
public function __construct(
public string $name,
public string $definition,
public TypeNode $type,
public bool $isNullable,
public ?string $fqcn = null,
) {}
}
20 changes: 0 additions & 20 deletions packages/generation/src/TypeScript/ResolvedType.php

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
use Tempest\Container\Container;
use Tempest\Generation\TypeScript\InterfaceDefinition;
use Tempest\Generation\TypeScript\PropertyDefinition;
use Tempest\Generation\TypeScript\ResolvedType;
use Tempest\Generation\TypeScript\StructureResolver;
use Tempest\Generation\TypeScript\TypeNodes\ArrayTypeNode;
use Tempest\Generation\TypeScript\TypeNodes\IntersectionTypeNode;
use Tempest\Generation\TypeScript\TypeNodes\PrimitiveTypeNode;
use Tempest\Generation\TypeScript\TypeNodes\TypeNode;
use Tempest\Generation\TypeScript\TypeNodes\UnionTypeNode;
use Tempest\Generation\TypeScript\TypeScriptGenerationConfig;
use Tempest\Generation\TypeScript\TypeScriptGenerator;
use Tempest\Reflection\PropertyReflector;
Expand Down Expand Up @@ -48,62 +52,56 @@ private function resolveProperty(PropertyReflector $property, TypeScriptGenerato
$elementTypeReflector = $property->getIterableType();

if ($elementTypeReflector instanceof TypeReflector) {
$result = $this->resolveType($elementTypeReflector, $generator);
$resolvedType = $this->resolveType($elementTypeReflector, $generator);

return new PropertyDefinition(
name: $property->getName(),
definition: $result->type . '[]',
type: new ArrayTypeNode($resolvedType),
isNullable: $property->isNullable(),
fqcn: $result->fqcn,
);
}

return new PropertyDefinition(
name: $property->getName(),
definition: 'any[]',
type: new ArrayTypeNode(new PrimitiveTypeNode('any')),
isNullable: $property->isNullable(),
);
}

if ($type->isUnion() || $type->isIntersection()) {
$parts = $type->split();
$resolvedTypes = [];
$referencedClasses = [];

foreach ($parts as $part) {
if ($part->getName() === 'null') {
continue;
}

$result = $this->resolveType($part, $generator);
$resolvedTypes[] = $result->type;

if ($result->fqcn !== null) {
$referencedClasses[] = $result->fqcn;
}
$resolvedTypes[] = $this->resolveType($part, $generator);
}

$symbol = $type->isIntersection() ? '&' : '|';

return new PropertyDefinition(
name: $property->getName(),
definition: implode(" {$symbol} ", $resolvedTypes),
type: match (true) {
$resolvedTypes === [] => new PrimitiveTypeNode('any'),
count($resolvedTypes) === 1 => $resolvedTypes[0],
$type->isIntersection() => new IntersectionTypeNode($resolvedTypes),
default => new UnionTypeNode($resolvedTypes),
},
isNullable: $property->isNullable(),
fqcn: count($referencedClasses) === 1 ? $referencedClasses[0] : null,
);
}

$result = $this->resolveType($type, $generator);
$resolvedType = $this->resolveType($type, $generator);

return new PropertyDefinition(
name: $property->getName(),
definition: $result->type,
type: $resolvedType,
isNullable: $property->isNullable(),
fqcn: $result->fqcn,
);
}

private function resolveType(TypeReflector $type, TypeScriptGenerator $generator): ResolvedType
private function resolveType(TypeReflector $type, TypeScriptGenerator $generator): TypeNode
{
foreach ($this->config->resolvers as $resolverClass) {
$resolver = $this->container->get($resolverClass);
Expand All @@ -113,6 +111,6 @@ private function resolveType(TypeReflector $type, TypeScriptGenerator $generator
}
}

return new ResolvedType('any');
return new PrimitiveTypeNode('any');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use Tempest\Container\Container;
use Tempest\Generation\TypeScript\StructureResolver;
use Tempest\Generation\TypeScript\TypeDefinition;
use Tempest\Generation\TypeScript\TypeNodes\TypeNode;
use Tempest\Generation\TypeScript\TypeNodes\UnionTypeNode;
use Tempest\Generation\TypeScript\TypeScriptGenerationConfig;
use Tempest\Generation\TypeScript\TypeScriptGenerator;
use Tempest\Reflection\TypeReflector;
Expand All @@ -26,29 +28,30 @@ public function __construct(

public function resolve(TypeReflector $type, TypeScriptGenerator $generator): TypeDefinition
{
$typeScriptType = implode(
separator: ' | ',
array: array_map(
callback: fn (ReflectionEnumUnitCase|ReflectionEnumBackedCase $case) => $this->resolveType(new TypeReflector($case), $generator),
array: $type->asEnum()->getReflectionCases(),
),
$types = array_map(
callback: fn (ReflectionEnumUnitCase|ReflectionEnumBackedCase $case) => $this->resolveType(new TypeReflector($case), $generator),
array: $type->asEnum()->getReflectionCases(),
);

$resolvedType = count($types) === 1
? $types[0]
: new UnionTypeNode($types);

return new TypeDefinition(
class: $type->getName(),
originalType: $type,
definition: $typeScriptType,
type: $resolvedType,
isNullable: $type->isNullable(),
);
}

private function resolveType(TypeReflector $type, TypeScriptGenerator $generator): string
private function resolveType(TypeReflector $type, TypeScriptGenerator $generator): TypeNode
{
foreach ($this->config->resolvers as $resolverClass) {
$resolver = $this->container->get($resolverClass);

if ($resolver->canResolve($type)) {
return $resolver->resolve($type, $generator)->type;
return $resolver->resolve($type, $generator);
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/generation/src/TypeScript/TypeDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Tempest\Generation\TypeScript;

use Tempest\Generation\TypeScript\TypeNodes\TypeNode;
use Tempest\Reflection\TypeReflector;
use Tempest\Support\Str;

Expand All @@ -25,7 +26,7 @@ final class TypeDefinition
public function __construct(
public string $class,
public TypeReflector $originalType,
public string $definition,
public TypeNode $type,
public bool $isNullable,
) {}
}
85 changes: 85 additions & 0 deletions packages/generation/src/TypeScript/TypeNodeRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\TypeScript;

use Tempest\Generation\TypeScript\TypeNodes\ArrayTypeNode;
use Tempest\Generation\TypeScript\TypeNodes\IntersectionTypeNode;
use Tempest\Generation\TypeScript\TypeNodes\LiteralTypeNode;
use Tempest\Generation\TypeScript\TypeNodes\ObjectTypeNode;
use Tempest\Generation\TypeScript\TypeNodes\ObjectTypePropertyNode;
use Tempest\Generation\TypeScript\TypeNodes\PrimitiveTypeNode;
use Tempest\Generation\TypeScript\TypeNodes\RawTypeNode;
use Tempest\Generation\TypeScript\TypeNodes\SymbolTypeNode;
use Tempest\Generation\TypeScript\TypeNodes\TypeNode;
use Tempest\Generation\TypeScript\TypeNodes\UnionTypeNode;

/**
* Renders semantic type nodes to TypeScript text.
*/
final readonly class TypeNodeRenderer
{
/**
* @param callable(string):string $symbolRenderer
*/
public function render(TypeNode $type, callable $symbolRenderer): string
{
return match (true) {
$type instanceof PrimitiveTypeNode => $type->name,
$type instanceof RawTypeNode => $type->expression,
$type instanceof LiteralTypeNode => $this->renderLiteral($type),
$type instanceof SymbolTypeNode => $symbolRenderer($type->fqcn),
$type instanceof ArrayTypeNode => $this->render($type->type, $symbolRenderer) . '[]',
$type instanceof ObjectTypeNode => $this->renderObjectType($type, $symbolRenderer),
$type instanceof UnionTypeNode => implode(' | ', array_map(
callback: fn (TypeNode $part): string => $this->render($part, $symbolRenderer),
array: $type->types,
)),
$type instanceof IntersectionTypeNode => implode(' & ', array_map(
callback: fn (TypeNode $part): string => $this->render($part, $symbolRenderer),
array: $type->types,
)),
default => 'any',
};
}

private function renderLiteral(LiteralTypeNode $type): string
{
if (is_string($type->value)) {
return sprintf("'%s'", addcslashes($type->value, "\\'\n\r\t\v\f"));
}

if (is_bool($type->value)) {
return $type->value ? 'true' : 'false';
}

return (string) $type->value;
}

/**
* @param callable(string):string $symbolRenderer
*/
private function renderObjectType(ObjectTypeNode $type, callable $symbolRenderer): string
{
$properties = array_map(
callback: fn (ObjectTypePropertyNode $property): string => vsprintf('%s%s: %s', [
$this->renderObjectPropertyName($property->name),
$property->optional ? '?' : '',
$this->render($property->type, $symbolRenderer),
]),
array: $type->properties,
);

return '{ ' . implode('; ', $properties) . '; }';
}

private function renderObjectPropertyName(string $name): string
{
if (preg_match('/^[A-Za-z_$][A-Za-z0-9_$]*$/', $name) === 1) {
return $name;
}

return sprintf("'%s'", addcslashes($name, "\\'\n\r\t\v\f"));
}
}
16 changes: 16 additions & 0 deletions packages/generation/src/TypeScript/TypeNodes/ArrayTypeNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\TypeScript\TypeNodes;

final class ArrayTypeNode implements TypeNode
{
public function __construct(
public readonly TypeNode $type,
) {}

public array $references {
get => $this->type->references;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\TypeScript\TypeNodes;

final class IntersectionTypeNode implements TypeNode
{
/**
* @param TypeNode[] $types
*/
public function __construct(
public readonly array $types,
) {}

public array $references {
get {
$fqcns = [];

foreach ($this->types as $type) {
$fqcns = [...$fqcns, ...$type->references];
}

return array_values(array_unique($fqcns));
}
}
}
Loading
Loading