Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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