Skip to content

Commit 42f0b31

Browse files
committed
Add support for expanding dot notation in TypeScript transformer.
1 parent 5fbe34a commit 42f0b31

File tree

3 files changed

+150
-8
lines changed

3 files changed

+150
-8
lines changed

docs/advanced-usage/typescript.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,53 @@ class DataObject extends Data
128128
}
129129
}
130130
```
131+
132+
### Dotted Notation Expansion
133+
134+
When using the `MapOutputName` or `MapName` attributes with dot notation (e.g., 'user.name'), the TypeScript transformer will respect the `expand_dot_notation` configuration setting:
135+
136+
```php
137+
class UserData extends Data
138+
{
139+
public function __construct(
140+
#[MapOutputName('user.name')]
141+
public string $name,
142+
#[MapOutputName('user.profile.bio')]
143+
public string $userBio,
144+
) {
145+
}
146+
}
147+
```
148+
149+
With `expand_dot_notation` disabled (default), this will generate:
150+
151+
```tsx
152+
{
153+
'user.name': string;
154+
'user.profile.bio': string;
155+
}
156+
```
157+
158+
When you enable `expand_dot_notation` in your `config/data.php` file:
159+
160+
```php
161+
'features' => [
162+
// Other features...
163+
'expand_dot_notation' => true,
164+
],
165+
```
166+
167+
The same data object will generate a nested TypeScript interface:
168+
169+
```tsx
170+
{
171+
user: {
172+
name: string;
173+
profile: {
174+
bio: string;
175+
};
176+
};
177+
}
178+
```
179+
180+
This ensures that your TypeScript interfaces match the structure of your JSON output when using dotted notation expansion.

src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Spatie\LaravelData\Support\TypeScriptTransformer;
44

5+
use Illuminate\Support\Arr;
56
use phpDocumentor\Reflection\Fqsen;
67
use phpDocumentor\Reflection\Type;
78
use phpDocumentor\Reflection\Types\Array_;
@@ -55,9 +56,9 @@ protected function transformProperties(
5556

5657
$isOptional = $dataClass->attributes->has(TypeScriptOptional::class);
5758

58-
return array_reduce(
59+
$results = array_reduce(
5960
$this->resolveProperties($class),
60-
function (string $carry, ReflectionProperty $property) use ($isOptional, $dataClass, $missingSymbols) {
61+
function (array $carry, ReflectionProperty $property) use ($isOptional, $dataClass, $missingSymbols) {
6162
/** @var \Spatie\LaravelData\Support\DataProperty $dataProperty */
6263
$dataProperty = $dataClass->properties[$property->getName()];
6364

@@ -88,16 +89,26 @@ function (string $carry, ReflectionProperty $property) use ($isOptional, $dataCl
8889

8990
$propertyName = $dataProperty->outputMappedName ?? $dataProperty->name;
9091

91-
if (! preg_match('/^[$_a-zA-Z][$_a-zA-Z0-9]*$/', $propertyName)) {
92-
$propertyName = "'{$propertyName}'";
92+
if (config('data.features.expand_dot_notation') && str_contains($propertyName, '.')) {
93+
Arr::set(
94+
$carry,
95+
$propertyName,
96+
$isOptional
97+
? "?: {$transformed}"
98+
: ": {$transformed}",
99+
);
100+
} else {
101+
$carry[$propertyName] = $isOptional
102+
? "?: {$transformed}"
103+
: ": {$transformed}";
93104
}
94105

95-
return $isOptional
96-
? "{$carry}{$propertyName}?: {$transformed};".PHP_EOL
97-
: "{$carry}{$propertyName}: {$transformed};".PHP_EOL;
106+
return $carry;
98107
},
99-
''
108+
[]
100109
);
110+
111+
return $this->arrayToTypeScript($results);
101112
}
102113

103114
protected function resolveTypeForProperty(
@@ -189,4 +200,23 @@ protected function cursorPaginatedCollectionType(string $class): Type
189200
]),
190201
]);
191202
}
203+
204+
protected function arrayToTypeScript(array $array): string
205+
{
206+
$carry = '';
207+
208+
foreach ($array as $propertyName => $value) {
209+
if (! preg_match('/^[$_a-zA-Z][$_a-zA-Z0-9]*$/', $propertyName)) {
210+
$propertyName = "'{$propertyName}'";
211+
}
212+
213+
if (is_array($value)) {
214+
$carry .= "{$propertyName}: {".PHP_EOL.$this->arrayToTypeScript($value).'};'.PHP_EOL;
215+
} else {
216+
$carry .= "{$propertyName}{$value};".PHP_EOL;
217+
}
218+
}
219+
220+
return $carry;
221+
}
192222
}

tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,65 @@ public function __construct(
340340
expect($transformer->canTransform($reflection))->toBeTrue();
341341
assertMatchesSnapshot($transformer->transform($reflection, 'DataObject')->transformed);
342342
});
343+
344+
it('handles dotted property names with expand_dot_notation disabled', function () {
345+
$config = TypeScriptTransformerConfig::create();
346+
config()->set('data.features.expand_dot_notation', false);
347+
348+
$data = new class ('hello', 'world') extends Data {
349+
public function __construct(
350+
#[MapOutputName('user.name')]
351+
public string $userName,
352+
#[MapOutputName('user.profile.bio')]
353+
public string $userBio,
354+
) {
355+
}
356+
};
357+
358+
$transformer = new DataTypeScriptTransformer($config);
359+
$reflection = new ReflectionClass($data);
360+
361+
expect($transformer->canTransform($reflection))->toBeTrue();
362+
$this->assertEquals(
363+
<<<TXT
364+
{
365+
'user.name': string;
366+
'user.profile.bio': string;
367+
}
368+
TXT,
369+
$transformer->transform($reflection, 'DataObject')->transformed
370+
);
371+
});
372+
373+
it('handles dotted property names with expand_dot_notation enabled', function () {
374+
$config = TypeScriptTransformerConfig::create();
375+
config()->set('data.features.expand_dot_notation', true);
376+
377+
$data = new class ('hello', 'world') extends Data {
378+
public function __construct(
379+
#[MapOutputName('user.name')]
380+
public string $userName,
381+
#[MapOutputName('user.profile.bio')]
382+
public string $userBio,
383+
) {
384+
}
385+
};
386+
387+
$transformer = new DataTypeScriptTransformer($config);
388+
$reflection = new ReflectionClass($data);
389+
390+
expect($transformer->canTransform($reflection))->toBeTrue();
391+
$this->assertEquals(
392+
<<<TXT
393+
{
394+
user: {
395+
name: string;
396+
profile: {
397+
bio: string;
398+
};
399+
};
400+
}
401+
TXT,
402+
$transformer->transform($reflection, 'DataObject')->transformed
403+
);
404+
});

0 commit comments

Comments
 (0)