Skip to content
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
/packages/discovery/ @brendt @aidan-casey
/packages/event-bus/ @brendt @aidan-casey
/packages/generation/ @brendt
/packages/generation/src/TypeScript @innocenzi
/packages/http/ @brendt @aidan-casey
/packages/http-client/ @aidan-casey
/packages/icon/ @innocenzi
Expand Down
107 changes: 107 additions & 0 deletions docs/2-features/18-typescript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: TypeScript
description: "Tempest provides the ability to generate TypeScript interfaces from PHP classes to ease integration with TypeScript-based front-ends."
keywords: ["Experimental", "Generation"]
experimental: true
---

## Overview

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.

Tempest solves this by automatically generating TypeScript definitions from your PHP value objects, data transfer objects, and enums.

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.

## Generating types

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.

By default, all application enums are also included automatically without needing an attribute. Generate your TypeScript definitions by running `generate:typescript-types`:

```sh ">_ generate:typescript-types"
✓ // Generated 14 type definitions across 2 namespaces.
```

This command scans your marked classes, generates the corresponding TypeScript definitions, and writes them to your configured output location.

## Customizing type resolution

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.

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

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

public function resolve(TypeReflector $type, TypeScriptGenerator $generator): ResolvedType
{
return new ResolvedType(match ($type->getName()) {
'string' => 'string',
'int', 'float' => 'number',
'bool' => 'boolean',
});
}
}
```

:::info
Type resolvers are automatically [discovered](../1-essentials/05-discovery.md) and do not need to be registered manually.
:::

## Configuring output location

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.

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.

### Single file output

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.

```php
use Tempest\Generation\TypeScript\Writers\NamespacedTypeScriptGenerationConfig;

return new NamespacedTypeScriptGenerationConfig(
filename: 'types.d.ts',
);
```

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`:

```json
{
"include": ["types.d.ts"]
}
```

You may then reference the generated types globally by using their namespaces:

```ts
defineProps<{
entry: Module.Changelog.ChangelogEntry
}>()
```

### Directory structure output

If you prefer to mirror your PHP namespace structure in separate files, you may return a {b`Tempest\Generation\TypeScript\Writers\DirectoryTypeScriptGenerationConfig`} configuration object:

```php
use Tempest\Generation\TypeScript\Writers\DirectoryTypeScriptGenerationConfig;

return new DirectoryTypeScriptGenerationConfig(
directory: 'src/Web/types',
);
```

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.
13 changes: 13 additions & 0 deletions packages/generation/src/TypeScript/AsType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Tempest\Generation\TypeScript;

use Attribute;

/**
* Marks this class as a source for TypeScript type generation.
*/
#[Attribute(Attribute::TARGET_CLASS)]
final class AsType
{
}
45 changes: 45 additions & 0 deletions packages/generation/src/TypeScript/GenerateTypesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\TypeScript;

use Tempest\Console\ConsoleCommand;
use Tempest\Console\HasConsole;
use Tempest\Container\Container;

final class GenerateTypesCommand
{
use HasConsole;

public function __construct(
private readonly TypeScriptGenerationConfig $config,
private readonly TypeScriptGenerator $generator,
private readonly Container $container,
) {}

#[ConsoleCommand(
name: 'generate:typescript-types',
description: 'Generate TypeScript types from PHP classes.',
)]
public function __invoke(): void
{
$this->console->writeln();

$output = $this->generator->generate();

if ($output->isEmpty()) {
$this->console->warning('No types found to generate.');
return;
}

$writer = $this->container->get($this->config->writer);
$writer->write($output);

$this->console->success(sprintf(
'Generated %d type definitions across %d namespaces.',
count($output->getAllDefinitions()),
count($output->getNamespaces()),
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\TypeScript;

use Tempest\Generation\TypeScript\StructureResolvers\ClassStructureResolver;
use Tempest\Generation\TypeScript\StructureResolvers\EnumStructureResolver;
use Tempest\Reflection\TypeReflector;

final class GenericTypeScriptGenerator implements TypeScriptGenerator
{
private ?TypesRepository $repository = null;

public function __construct(
private readonly TypeScriptGenerationConfig $config,
private readonly ClassStructureResolver $classResolver,
private readonly EnumStructureResolver $enumResolver,
) {}

public function generate(): TypeScriptOutput
{
$this->repository = new TypesRepository();

foreach ($this->config->sources as $className) {
$this->include($className);
}

$grouped = [];

foreach ($this->repository->getAll() as $definition) {
$namespace = $definition->namespace;
$grouped[$namespace] ??= [];
$grouped[$namespace][] = $definition;
}

ksort($grouped);

return new TypeScriptOutput(
namespaces: $grouped,
);
}

public function include(string $className): void
{
if ($this->repository->has($className)) {
return;
}

$type = new TypeReflector($className);

if ($type->isEnum()) {
$this->repository->add($this->enumResolver->resolve($type, $this));
return;
}

if ($type->isClass() || $type->isInterface()) {
$this->repository->add($this->classResolver->resolve($type, $this));
return;
}
}
}
33 changes: 33 additions & 0 deletions packages/generation/src/TypeScript/InterfaceDefinition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\TypeScript;

use Tempest\Reflection\TypeReflector;
use Tempest\Support\Str;

/**
* Represents a TypeScript interface definition generated from a PHP class.
*/
final class InterfaceDefinition
{
public string $namespace {
get {
if (! Str\contains($this->class, '\\')) {
return '';
}

return Str\before_last($this->class, '\\');
}
}

/**
* @param PropertyDefinition[] $properties
*/
public function __construct(
public string $class,
public TypeReflector $originalType,
public array $properties,
) {}
}
23 changes: 23 additions & 0 deletions packages/generation/src/TypeScript/PropertyDefinition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\TypeScript;

/**
* 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.
*/
public function __construct(
public string $name,
public string $definition,
public bool $isNullable,
public ?string $fqcn = null,
) {}
}
20 changes: 20 additions & 0 deletions packages/generation/src/TypeScript/ResolvedType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\TypeScript;

/**
* Represents a PHP type resolved to a TypeScript one as a string.
*/
final readonly class ResolvedType
{
/**
* @param string $type A resolved TypeScript type.
* @param null|string $fqcn The PHP FQCN of the original type.
*/
public function __construct(
public string $type,
public ?string $fqcn = null,
) {}
}
15 changes: 15 additions & 0 deletions packages/generation/src/TypeScript/StructureResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Tempest\Generation\TypeScript;

use Tempest\Reflection\TypeReflector;

interface StructureResolver
{
/**
* Resolves a PHP type into a TypeScript definition.
*/
public function resolve(TypeReflector $type, TypeScriptGenerator $generator): TypeDefinition|InterfaceDefinition;
}
Loading