-
-
Notifications
You must be signed in to change notification settings - Fork 259
Description
✏️ Describe the bug
When using Lazy|Type in property type hints with Lazy::closure(), the TypeScript transformer incorrectly marks properties as optional. However, using ClosureLazy|Type works correctly and keeps properties required.
The root cause is in DataTypeScriptTransformer.php line 78, which uses exact string comparison (!==) instead of inheritance checking:
|| ($dataProperty->type->lazyType && $dataProperty->type->lazyType !== ClosureLazy::class)When you write Lazy|Type, the DataTypeFactory captures Lazy::class. Since "Lazy" !== "ClosureLazy", the property is marked as optional in TypeScript.
↪️ To Reproduce
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Lazy;
use Spatie\LaravelData\Support\Lazy\ClosureLazy;
it('marks Lazy union properties as optional in TypeScript incorrectly', function () {
class MemberData extends Data
{
public function __construct(
public string $name,
public string $email,
) {}
}
// Using abstract Lazy class in type hint
class ShowDataWithLazy extends Data
{
public function __construct(
public Lazy|MemberData $member,
) {}
}
// Using concrete ClosureLazy class in type hint
class ShowDataWithClosureLazy extends Data
{
public function __construct(
public ClosureLazy|MemberData $member,
) {}
}
// Both use Lazy::closure() at runtime
$dataWithLazy = ShowDataWithLazy::from([
'member' => Lazy::closure(fn () => MemberData::from(['name' => 'John', 'email' => '[email protected]'])),
]);
$dataWithClosureLazy = ShowDataWithClosureLazy::from([
'member' => Lazy::closure(fn () => MemberData::from(['name' => 'John', 'email' => '[email protected]'])),
]);
// Generate TypeScript for both
$transformer = app(\Spatie\LaravelData\Support\TypeScriptTransformer\DataTypeScriptTransformer::class);
// ShowDataWithLazy will have member?: MemberData (optional)
// ShowDataWithClosureLazy will have member: MemberData (required)
dd([
'with_Lazy_generates_optional' => 'member?: MemberData',
'with_ClosureLazy_generates_required' => 'member: MemberData',
]);
});Generated TypeScript output:
With Lazy|Type:
export type ShowDataWithLazy = {
member?: MemberData; // ❌ Optional when it shouldn't be
};With ClosureLazy|Type:
export type ShowDataWithClosureLazy = {
member: MemberData; // ✅ Required as expected
};✅ Expected behavior
Properties using Lazy::closure() should NOT be marked as optional in TypeScript, regardless of whether the type hint is Lazy|Type or ClosureLazy|Type, since ClosureLazy is specifically excluded from the optional check.
Both classes should generate the same required TypeScript property:
member: MemberData; // Required, not optionalSuggested fix:
Change the check in DataTypeScriptTransformer.php line 78 from:
|| ($dataProperty->type->lazyType && $dataProperty->type->lazyType !== ClosureLazy::class)To:
|| ($dataProperty->type->lazyType && !is_a($dataProperty->type->lazyType, ClosureLazy::class, true))This would properly check if lazyType is ClosureLazy or a subclass, rather than requiring exact equality.
🖥️ Versions
Laravel: 12.x
Laravel Data: 4.15.1 (also affects 4.18.0)
PHP: 8.3.23