Skip to content

Commit 744b7f9

Browse files
innocenzibrendt
andauthored
feat(generation): add support for TypeScript types generation (#1897)
Co-authored-by: Brent Roose <brent.roose@gmail.com>
1 parent 5ebee16 commit 744b7f9

46 files changed

Lines changed: 2433 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
/packages/discovery/ @brendt @aidan-casey
2828
/packages/event-bus/ @brendt @aidan-casey
2929
/packages/generation/ @brendt
30+
/packages/generation/src/TypeScript @innocenzi
3031
/packages/http/ @brendt @aidan-casey
3132
/packages/http-client/ @aidan-casey
3233
/packages/icon/ @innocenzi

docs/2-features/18-typescript.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
title: TypeScript
3+
description: "Tempest provides the ability to generate TypeScript interfaces from PHP classes to ease integration with TypeScript-based front-ends."
4+
keywords: ["Experimental", "Generation"]
5+
experimental: true
6+
---
7+
8+
## Overview
9+
10+
When building applications with TypeScript-based front-ends like [Inertia](https://inertiajs.com), keeping your client-side types synchronized with your PHP backend can be tedious and error-prone.
11+
12+
Tempest solves this by automatically generating TypeScript definitions from your PHP value objects, data transfer objects, and enums.
13+
14+
You can choose to output a single `.d.ts` declaration file or a directory tree of individual `.ts` modules, depending on your project's needs.
15+
16+
## Generating types
17+
18+
Mark any PHP class with the {b`#[Tempest\Generation\TypeScript\AsType]`} attribute to instruct Tempest that a matching TypeScript interface must be generated based on its public properties.
19+
20+
By default, all application enums are also included automatically without needing an attribute. Generate your TypeScript definitions by running `generate:typescript-types`:
21+
22+
```sh ">_ generate:typescript-types"
23+
✓ // Generated 14 type definitions across 2 namespaces.
24+
```
25+
26+
This command scans your marked classes, generates the corresponding TypeScript definitions, and writes them to your configured output location.
27+
28+
## Customizing type resolution
29+
30+
Tempest provides several built-in type resolvers for common types: strings, numbers, dates, enums and class references.
31+
32+
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.
33+
34+
The following is the actual implementation of the built-in resolver that handles scalar types:
35+
36+
```php ScalarTypeResolver.php
37+
#[Priority(Priority::LOW)]
38+
final class ScalarTypeResolver implements TypeResolver
39+
{
40+
public function canResolve(TypeReflector $type): bool
41+
{
42+
return $type->isBuiltIn()
43+
&& in_array($type->getName(), ['string', 'int', 'float', 'bool'], strict: true);
44+
}
45+
46+
public function resolve(TypeReflector $type, TypeScriptGenerator $generator): ResolvedType
47+
{
48+
return new ResolvedType(match ($type->getName()) {
49+
'string' => 'string',
50+
'int', 'float' => 'number',
51+
'bool' => 'boolean',
52+
});
53+
}
54+
}
55+
```
56+
57+
:::info
58+
Type resolvers are automatically [discovered](../1-essentials/05-discovery.md) and do not need to be registered manually.
59+
:::
60+
61+
## Configuring output location
62+
63+
By default, Tempest generates a `types.d.ts` definition file at the root of the project, in which the generated types are organized by namespace.
64+
65+
This may be configured by creating a `typescript.config.php` [configuration file](../1-essentials/06-configuration.md#configuration-files) and returning one of the available configuration objects.
66+
67+
### Single file output
68+
69+
To keep all of the TypeScript definitions in a single `.d.ts` declaration file, which is the default, return a {b`Tempest\Generation\TypeScript\Writers\NamespacedTypeScriptGenerationConfig`} object and specify the desired output filename.
70+
71+
```php
72+
use Tempest\Generation\TypeScript\Writers\NamespacedTypeScriptGenerationConfig;
73+
74+
return new NamespacedTypeScriptGenerationConfig(
75+
filename: 'types.d.ts',
76+
);
77+
```
78+
79+
The declaration file should be automatically picked up by TypeScript—if not, ensure that it's included in the `include` property of your `tsconfig.json`:
80+
81+
```json
82+
{
83+
"include": ["types.d.ts"]
84+
}
85+
```
86+
87+
You may then reference the generated types globally by using their namespaces:
88+
89+
```ts
90+
defineProps<{
91+
entry: Module.Changelog.ChangelogEntry
92+
}>()
93+
```
94+
95+
### Directory structure output
96+
97+
If you prefer to mirror your PHP namespace structure in separate files, you may return a {b`Tempest\Generation\TypeScript\Writers\DirectoryTypeScriptGenerationConfig`} configuration object:
98+
99+
```php
100+
use Tempest\Generation\TypeScript\Writers\DirectoryTypeScriptGenerationConfig;
101+
102+
return new DirectoryTypeScriptGenerationConfig(
103+
directory: 'src/Web/types',
104+
);
105+
```
106+
107+
This creates a directory tree of individual `.ts` files, making it easier to navigate your types. Each namespace gets its own file, and imports between files are handled automatically.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\Generation\TypeScript;
4+
5+
use Attribute;
6+
7+
/**
8+
* Marks this class as a source for TypeScript type generation.
9+
*/
10+
#[Attribute(Attribute::TARGET_CLASS)]
11+
final class AsType
12+
{
13+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Generation\TypeScript;
6+
7+
use Tempest\Console\ConsoleCommand;
8+
use Tempest\Console\HasConsole;
9+
use Tempest\Container\Container;
10+
11+
final class GenerateTypesCommand
12+
{
13+
use HasConsole;
14+
15+
public function __construct(
16+
private readonly TypeScriptGenerationConfig $config,
17+
private readonly TypeScriptGenerator $generator,
18+
private readonly Container $container,
19+
) {}
20+
21+
#[ConsoleCommand(
22+
name: 'generate:typescript-types',
23+
description: 'Generate TypeScript types from PHP classes.',
24+
)]
25+
public function __invoke(): void
26+
{
27+
$this->console->writeln();
28+
29+
$output = $this->generator->generate();
30+
31+
if ($output->isEmpty()) {
32+
$this->console->warning('No types found to generate.');
33+
return;
34+
}
35+
36+
$writer = $this->container->get($this->config->writer);
37+
$writer->write($output);
38+
39+
$this->console->success(sprintf(
40+
'Generated %d type definitions across %d namespaces.',
41+
count($output->getAllDefinitions()),
42+
count($output->getNamespaces()),
43+
));
44+
}
45+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Generation\TypeScript;
6+
7+
use Tempest\Generation\TypeScript\StructureResolvers\ClassStructureResolver;
8+
use Tempest\Generation\TypeScript\StructureResolvers\EnumStructureResolver;
9+
use Tempest\Reflection\TypeReflector;
10+
11+
final class GenericTypeScriptGenerator implements TypeScriptGenerator
12+
{
13+
private ?TypesRepository $repository = null;
14+
15+
public function __construct(
16+
private readonly TypeScriptGenerationConfig $config,
17+
private readonly ClassStructureResolver $classResolver,
18+
private readonly EnumStructureResolver $enumResolver,
19+
) {}
20+
21+
public function generate(): TypeScriptOutput
22+
{
23+
$this->repository = new TypesRepository();
24+
25+
foreach ($this->config->sources as $className) {
26+
$this->include($className);
27+
}
28+
29+
$grouped = [];
30+
31+
foreach ($this->repository->getAll() as $definition) {
32+
$namespace = $definition->namespace;
33+
$grouped[$namespace] ??= [];
34+
$grouped[$namespace][] = $definition;
35+
}
36+
37+
ksort($grouped);
38+
39+
return new TypeScriptOutput(
40+
namespaces: $grouped,
41+
);
42+
}
43+
44+
public function include(string $className): void
45+
{
46+
if ($this->repository->has($className)) {
47+
return;
48+
}
49+
50+
$type = new TypeReflector($className);
51+
52+
if ($type->isEnum()) {
53+
$this->repository->add($this->enumResolver->resolve($type, $this));
54+
return;
55+
}
56+
57+
if ($type->isClass() || $type->isInterface()) {
58+
$this->repository->add($this->classResolver->resolve($type, $this));
59+
return;
60+
}
61+
}
62+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Generation\TypeScript;
6+
7+
use Tempest\Reflection\TypeReflector;
8+
use Tempest\Support\Str;
9+
10+
/**
11+
* Represents a TypeScript interface definition generated from a PHP class.
12+
*/
13+
final class InterfaceDefinition
14+
{
15+
public string $namespace {
16+
get {
17+
if (! Str\contains($this->class, '\\')) {
18+
return '';
19+
}
20+
21+
return Str\before_last($this->class, '\\');
22+
}
23+
}
24+
25+
/**
26+
* @param PropertyDefinition[] $properties
27+
*/
28+
public function __construct(
29+
public string $class,
30+
public TypeReflector $originalType,
31+
public array $properties,
32+
) {}
33+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Generation\TypeScript;
6+
7+
/**
8+
* Represents a property in a TypeScript interface.
9+
*/
10+
final readonly class PropertyDefinition
11+
{
12+
/**
13+
* @param string $name The name of the property.
14+
* @param string $definition The TypeScript definition of the property.
15+
* @param null|string $fqcn The PHP FQCN of the original type.
16+
*/
17+
public function __construct(
18+
public string $name,
19+
public string $definition,
20+
public bool $isNullable,
21+
public ?string $fqcn = null,
22+
) {}
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Generation\TypeScript;
6+
7+
/**
8+
* Represents a PHP type resolved to a TypeScript one as a string.
9+
*/
10+
final readonly class ResolvedType
11+
{
12+
/**
13+
* @param string $type A resolved TypeScript type.
14+
* @param null|string $fqcn The PHP FQCN of the original type.
15+
*/
16+
public function __construct(
17+
public string $type,
18+
public ?string $fqcn = null,
19+
) {}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Generation\TypeScript;
6+
7+
use Tempest\Reflection\TypeReflector;
8+
9+
interface StructureResolver
10+
{
11+
/**
12+
* Resolves a PHP type into a TypeScript definition.
13+
*/
14+
public function resolve(TypeReflector $type, TypeScriptGenerator $generator): TypeDefinition|InterfaceDefinition;
15+
}

0 commit comments

Comments
 (0)