Skip to content

Commit 67b2376

Browse files
wip
1 parent a0f02f3 commit 67b2376

File tree

7 files changed

+507
-49
lines changed

7 files changed

+507
-49
lines changed

README.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ $config->transformer(new EnumTransformer(useNativeEnums: true)); // transformers
187187
```
188188

189189
Quick note: transformers are executed in the order they are registered in the configuration, when a transformer cannot
190-
transform a class, the next transformer is executed.
190+
transform a class, the next transformer is checked.
191191

192192
Transformers work on PHP classes, we need to tell TypeScript transformer where to look for these classes. This can be
193193
done by adding a directory to the configuration:
@@ -369,7 +369,7 @@ class Types
369369
}
370370
```
371371

372-
As you an see, when an array value is typed correctly, it will also be typed correctly in TypeScript.
372+
As you can see, when an array value is typed correctly, it will also be typed correctly in TypeScript.
373373

374374
It is also possible to use non-typical array key types, like an enum:
375375

@@ -381,6 +381,16 @@ class Types
381381
}
382382
```
383383

384+
It is possible to define array shapes like this:
385+
386+
```php
387+
class Types
388+
{
389+
/** @var array{age: int, name: string} */
390+
public array $property; // { age: number, name: string }
391+
}
392+
```
393+
384394
There are multiple locations where you can add property annotations:
385395

386396
```php
@@ -416,7 +426,7 @@ class Types
416426
}
417427
```
418428

419-
If an typed object is not transformed and thus we don't know how it will look like in TypeScript, it will be replaced
429+
If a typed object is not transformed and thus we don't know how it will look like in TypeScript, it will be replaced
420430
by `unknown`. It is possible to replace these unknown types with a TypeScript type, without transforming them, keep
421431
reading to learn how to do that.
422432

@@ -579,7 +589,7 @@ $config->replaceType(DateTimeInterface::class, function (TypeReference $referenc
579589
Internally the package uses TypeScript nodes to represent TypeScript types, these nodes can be used to build complex
580590
types and it is possible to create your own nodes.
581591

582-
For example, a TypeScript alias is representing a User object looks like this:
592+
For example, a TypeScript alias is representing a User object will look like this:
583593

584594
```php
585595
use Spatie\TypeScriptTransformer\TypeScriptNodes;
@@ -670,7 +680,7 @@ When a class cannot be transformed, the next transformer in the list will be exe
670680
Most of the time, transforming a class comes down to taking all the properties and transforming them to a TypeScript
671681
object with properties, the package provides an easy-to-extend class for this called `ClassTransformer`.
672682

673-
You can create your own by extending the `ClassTransformer` and implementing the `shouldTransform` method:
683+
You can create your own version by extending the `ClassTransformer` and implementing the `shouldTransform` method:
674684

675685
```php
676686
use Spatie\TypeScriptTransformer\Transformers\ClassTransformer;
@@ -817,7 +827,8 @@ A `TypesProvider` implements the `TypeProvider` interface:
817827
```php
818828
namespace Spatie\TypeScriptTransformer\TypeProviders;
819829

820-
use Spatie\TypeScriptTransformer\Collections\TransformedCollection;use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig;
830+
use Spatie\TypeScriptTransformer\Collections\TransformedCollection;
831+
use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig;
821832

822833
interface TypesProvider
823834
{
@@ -1196,7 +1207,7 @@ The steps look as following:
11961207
6. Write those files to disk
11971208
7. Format the files
11981209

1199-
The two hooking points above can be used to run a visitor on the collection of Transformed types:
1210+
The two hooking points below can be used to run a visitor on the collection of Transformed types:
12001211

12011212
```php
12021213
use Spatie\TypeScriptTransformer\Visitor\VisitorClosureType;

composer.json

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
],
1717
"require": {
1818
"php": "^8.2",
19-
"illuminate/contracts": "^10.0|^11.0",
20-
"phpstan/phpdoc-parser": "^1.13",
19+
"illuminate/contracts": "^12.0",
20+
"phpstan/phpdoc-parser": "^2.0",
2121
"roave/better-reflection": "^6.41",
2222
"spatie/file-system-watcher": "^1.1",
2323
"spatie/laravel-package-tools": "^1.14.0",
@@ -27,15 +27,16 @@
2727
"require-dev": {
2828
"friendsofphp/php-cs-fixer": "^3.0",
2929
"laravel/pint": "^1.0",
30-
"nunomaduro/collision": "^7.9",
31-
"nunomaduro/larastan": "^2.0.1",
32-
"orchestra/testbench": "^8.0|^9.0",
33-
"pestphp/pest": "^2.0",
34-
"pestphp/pest-plugin-arch": "^2.0",
35-
"pestphp/pest-plugin-laravel": "^2.0",
30+
"nunomaduro/collision": "^8.0",
31+
"nunomaduro/larastan": "^3.0",
32+
"orchestra/testbench": "^10.0",
33+
"pestphp/pest": "^3.0",
34+
"pestphp/pest-plugin-arch": "^3.0",
35+
"pestphp/pest-plugin-laravel": "^3.0",
3636
"phpstan/extension-installer": "^1.1",
37-
"phpstan/phpstan-deprecation-rules": "^1.0",
38-
"phpstan/phpstan-phpunit": "^1.0",
37+
"phpstan/phpstan-deprecation-rules": "^2.0",
38+
"phpstan/phpstan-phpunit": "^2.0",
39+
"phpstan/phpstan" : "^2.0",
3940
"spatie/laravel-ray": "^1.26",
4041
"spatie/pest-plugin-snapshots": "^2.1",
4142
"spatie/ray": "^1.41",

src/Actions/ParseUserDefinedTypeAction.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\PhpDocParser\Parser\ConstExprParser;
77
use PHPStan\PhpDocParser\Parser\TokenIterator;
88
use PHPStan\PhpDocParser\Parser\TypeParser;
9+
use PHPStan\PhpDocParser\ParserConfig;
910
use Spatie\TypeScriptTransformer\PhpNodes\PhpClassNode;
1011
use Spatie\TypeScriptTransformer\Support\Concerns\Instanceable;
1112
use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode;
@@ -17,11 +18,12 @@ class ParseUserDefinedTypeAction
1718
protected TypeParser $typeParser;
1819

1920
public function __construct(
20-
protected ConstExprParser $constExprParser = new ConstExprParser(),
21-
protected Lexer $lexer = new Lexer(),
21+
protected ParserConfig $parserConfig = new ParserConfig(usedAttributes: []),
22+
protected ConstExprParser $constExprParser = new ConstExprParser(new ParserConfig(usedAttributes: [])),
23+
protected Lexer $lexer = new Lexer(new ParserConfig(usedAttributes: [])),
2224
protected TranspilePhpStanTypeToTypeScriptNodeAction $transpilePhpStanTypeToTypeScriptNodeAction = new TranspilePhpStanTypeToTypeScriptNodeAction(),
2325
) {
24-
$this->typeParser = new TypeParser($constExprParser);
26+
$this->typeParser = new TypeParser($this->parserConfig, $constExprParser);
2527
}
2628

2729
public function execute(string $type, ?PhpClassNode $node = null): TypeScriptNode

src/Actions/TranspilePhpStanTypeToTypeScriptNodeAction.php

Lines changed: 135 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
namespace Spatie\TypeScriptTransformer\Actions;
44

55
use Exception;
6+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
7+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
68
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
79
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
810
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
11+
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
912
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
1013
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1114
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
@@ -24,6 +27,7 @@
2427
use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptGeneric;
2528
use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIdentifier;
2629
use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptIntersection;
30+
use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptLiteral;
2731
use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNode;
2832
use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNull;
2933
use Spatie\TypeScriptTransformer\TypeScriptNodes\TypeScriptNumber;
@@ -65,18 +69,52 @@ protected function identifierNode(
6569
return new TypeScriptAny();
6670
}
6771

68-
if ($node->name === 'string' || $node->name === 'class-string') {
72+
if ($node->name === 'string'
73+
|| $node->name === 'class-string'
74+
|| $node->name === 'interface-string'
75+
|| $node->name === 'trait-string'
76+
|| $node->name === 'callable-string'
77+
|| $node->name === 'enum-string'
78+
|| $node->name === 'lowercase-string'
79+
|| $node->name === 'uppercase-string'
80+
|| $node->name === 'literal-string'
81+
|| $node->name === 'numeric-string'
82+
|| $node->name === 'non-empty-string'
83+
|| $node->name === 'non-empty-lowercase-string'
84+
|| $node->name === 'non-empty-uppercase-string'
85+
|| $node->name === 'truthy-string'
86+
|| $node->name === 'non-falsy-string'
87+
|| $node->name === 'non-empty-literal-string'
88+
) {
6989
return new TypeScriptString();
7090
}
7191

72-
if ($node->name === 'float' || $node->name === 'double' || $node->name === 'int' || $node->name === 'integer') {
92+
if ($node->name === 'float'
93+
|| $node->name === 'double'
94+
|| $node->name === 'int'
95+
|| $node->name === 'integer'
96+
|| $node->name === 'positive-int'
97+
|| $node->name === 'negative-int'
98+
|| $node->name === 'non-positive-int'
99+
|| $node->name === 'non-negative-int'
100+
|| $node->name === 'non-zero-int'
101+
|| $node->name === 'numeric'
102+
) {
73103
return new TypeScriptNumber();
74104
}
75105

76106
if ($node->name === 'bool' || $node->name === 'boolean' || $node->name === 'true' || $node->name === 'false') {
77107
return new TypeScriptBoolean();
78108
}
79109

110+
if ($node->name === 'scalar') {
111+
return new TypeScriptUnion([
112+
new TypeScriptNumber(),
113+
new TypeScriptString(),
114+
new TypeScriptBoolean(),
115+
]);
116+
}
117+
80118
if ($node->name === 'void') {
81119
return new TypeScriptVoid();
82120
}
@@ -93,10 +131,6 @@ protected function identifierNode(
93131
return new TypeScriptNull();
94132
}
95133

96-
if ($node->name === 'self' || $node->name === 'static') {
97-
return new TypeReference(new ClassStringReference($phpClassNode->getName()));
98-
}
99-
100134
if ($node->name === 'object') {
101135
return new TypeScriptObject([]);
102136
}
@@ -108,24 +142,41 @@ protected function identifierNode(
108142
]);
109143
}
110144

111-
if (class_exists($node->name) || interface_exists($node->name)) {
112-
return new TypeReference(new ClassStringReference($node->name));
145+
$className = $this->resolveClass($node->name, $phpClassNode);
146+
147+
if ($className) {
148+
return new TypeReference(new ClassStringReference($className));
149+
}
150+
151+
return new TypeScriptUnknown();
152+
}
153+
154+
protected function resolveClass(
155+
string $className,
156+
?PhpClassNode $phpClassNode
157+
): ?string {
158+
if ($className === 'self' || $className === 'static' || $className === '$this') {
159+
return $phpClassNode?->getName();
160+
}
161+
162+
if (class_exists($className) || interface_exists($className)) {
163+
return $className;
113164
}
114165

115166
if ($phpClassNode === null) {
116-
return new TypeScriptUnknown();
167+
return null;
117168
}
118169

119170
$referenced = $this->findClassNameFqcnAction->execute(
120171
$phpClassNode,
121-
$node->name
172+
$className
122173
);
123174

124175
if (class_exists($referenced) || interface_exists($referenced)) {
125-
return new TypeReference(new ClassStringReference($referenced));
176+
return $referenced;
126177
}
127178

128-
return new TypeScriptUnknown();
179+
return null;
129180
}
130181

131182
protected function arrayTypeNode(
@@ -143,8 +194,13 @@ protected function arrayShapeNode(
143194
): TypeScriptObject {
144195
return new TypeScriptObject(array_map(
145196
function (ArrayShapeItemNode|ObjectShapeItemNode $item) use ($phpClassNode) {
197+
$name = match ($item->keyName::class) {
198+
IdentifierTypeNode::class => $item->keyName->name,
199+
ConstExprStringNode::class => $item->keyName->value,
200+
};
201+
146202
return new TypeScriptProperty(
147-
(string) $item->keyName,
203+
$name,
148204
$this->execute($item->valueType, $phpClassNode),
149205
isOptional: $item->optional
150206
);
@@ -194,26 +250,27 @@ protected function genericNode(
194250
GenericTypeNode $node,
195251
?PhpClassNode $phpClassNode
196252
): TypeScriptNode {
197-
if ($node->type->name === 'array' || $node->type->name === 'Array') {
253+
if ($node->type->name === 'array'
254+
|| $node->type->name === 'Array'
255+
|| $node->type->name === 'non-empty-array'
256+
|| $node->type->name === 'list'
257+
|| $node->type->name === 'non-empty-list'
258+
) {
198259
return $this->genericArrayNode($node, $phpClassNode);
199260
}
200261

201-
$type = $this->execute($node->type, $phpClassNode);
262+
if ($node->type->name === 'int') {
263+
return new TypeScriptNumber();
264+
}
202265

203-
if ($type instanceof TypeScriptString) {
204-
return $type; // class-string<something> case
266+
if ($node->type->name === 'key-of' || $node->type->name === 'value-of') {
267+
return $this->keyOrValueOfGenericNode($node, $phpClassNode);
205268
}
206269

207-
return new TypeScriptGeneric(
208-
$type,
209-
array_map(
210-
fn (TypeNode $type) => $this->execute($type, $phpClassNode),
211-
$node->genericTypes
212-
)
213-
);
270+
return $this->defaultGenericNode($node, $phpClassNode);
214271
}
215272

216-
private function genericArrayNode(GenericTypeNode $node, ?PhpClassNode $phpClassNode): TypeScriptGeneric|TypeScriptArray
273+
protected function genericArrayNode(GenericTypeNode $node, ?PhpClassNode $phpClassNode): TypeScriptGeneric|TypeScriptArray
217274
{
218275
$genericTypes = count($node->genericTypes);
219276

@@ -241,4 +298,57 @@ private function genericArrayNode(GenericTypeNode $node, ?PhpClassNode $phpClass
241298
[$key, $value,]
242299
);
243300
}
301+
302+
protected function keyOrValueOfGenericNode(GenericTypeNode $node, ?PhpClassNode $phpClassNode): TypeScriptNode
303+
{
304+
if (count($node->genericTypes) === 1
305+
&& $node->genericTypes[0] instanceof ConstTypeNode
306+
&& $node->genericTypes[0]->constExpr instanceof ConstFetchNode
307+
) {
308+
return $this->keyOrValueOfArrayConstNode($node, $phpClassNode, $node->genericTypes[0]->constExpr);
309+
}
310+
311+
312+
return $this->defaultGenericNode($node, $phpClassNode);
313+
}
314+
315+
protected function keyOrValueOfArrayConstNode(
316+
GenericTypeNode $node,
317+
?PhpClassNode $phpClassNode,
318+
ConstFetchNode $constFetchNode,
319+
): TypeScriptNode {
320+
$class = $this->resolveClass($constFetchNode->className, $phpClassNode);
321+
322+
if ($class === null) {
323+
return $this->defaultGenericNode($node, $phpClassNode);
324+
}
325+
326+
$array = $class::{$constFetchNode->name};
327+
328+
$items = $node->type->name === 'key-of'
329+
? array_keys($array)
330+
: array_values($array);
331+
332+
return new TypeScriptUnion(array_map(
333+
fn (mixed $key) => new TypeScriptLiteral($key),
334+
$items
335+
));
336+
}
337+
338+
protected function defaultGenericNode(GenericTypeNode $node, ?PhpClassNode $phpClassNode): TypeScriptNode
339+
{
340+
$type = $this->execute($node->type, $phpClassNode);
341+
342+
if ($type instanceof TypeScriptString) {
343+
return $type; // class-string<something> case
344+
}
345+
346+
return new TypeScriptGeneric(
347+
$type,
348+
array_map(
349+
fn (TypeNode $type) => $this->execute($type, $phpClassNode),
350+
$node->genericTypes
351+
)
352+
);
353+
}
244354
}

0 commit comments

Comments
 (0)