Skip to content

Commit fb47a48

Browse files
veeweeondrejmirtes
authored andcommitted
Add support for Psl\Type\nullish() in shape types
Nullish wraps a type as T|null while keeping the key required, unlike optional() which makes the key absent. Supports both standalone nullish(T) and combined optional(nullish(T)).
1 parent 55a3ef4 commit fb47a48

7 files changed

Lines changed: 146 additions & 1 deletion

File tree

extension.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
parameters:
22
stubFiles:
33
- stubs/Option.stub
4+
- stubs/nullish.stub
5+
- stubs/NullishType.stub
46
- stubs/optional.stub
57
- stubs/OptionalType.stub
68
- stubs/Type.stub

src/Type/TypeShapeReturnTypeExtension.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Type\Generic\GenericObjectType;
1313
use PHPStan\Type\Type;
1414
use PHPStan\Type\TypeCombinator;
15+
use Psl\Type\Internal\NullishType;
1516
use Psl\Type\Internal\OptionalType;
1617
use Psl\Type\TypeInterface;
1718
use function count;
@@ -55,14 +56,29 @@ private function createResult(ConstantArrayType $arrayType): Type
5556
$builder = ConstantArrayTypeBuilder::createEmpty();
5657
foreach ($arrayType->getKeyTypes() as $key) {
5758
$valueType = $arrayType->getOffsetValueType($key);
58-
[$type, $optional] = $this->extractOptional($valueType->getTemplateType(TypeInterface::class, 'T'));
59+
$templateType = $valueType->getTemplateType(TypeInterface::class, 'T');
60+
[$type, $optional] = $this->extractOptional($templateType);
61+
[$type] = $this->extractNullish($type);
5962

6063
$builder->setOffsetValueType($key, $type, $optional);
6164
}
6265

6366
return $builder->getArray();
6467
}
6568

69+
/**
70+
* @return array{Type, bool}
71+
*/
72+
private function extractNullish(Type $type): array
73+
{
74+
$nullishType = $type->getTemplateType(NullishType::class, 'T');
75+
if ($nullishType instanceof ErrorType) {
76+
return [$type, false];
77+
}
78+
79+
return [TypeCombinator::addNull($nullishType), false];
80+
}
81+
6682
/**
6783
* @return array{Type, bool}
6884
*/

stubs/NullishType.stub

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Psl\Type\Internal;
4+
5+
use Psl\Type;
6+
7+
/**
8+
* @template T
9+
*
10+
* @extends Type\Type<T>
11+
*
12+
* @internal
13+
*/
14+
final class NullishType extends Type\Type
15+
{
16+
17+
}

stubs/nullish.stub

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Psl\Type;
4+
5+
use Psl\Type\Internal\NullishType;
6+
7+
/**
8+
* @template T
9+
*
10+
* @param TypeInterface<T> $inner_type
11+
*
12+
* @return TypeInterface<NullishType<T>>
13+
*/
14+
function nullish(TypeInterface $inner_type): TypeInterface
15+
{
16+
17+
}

tests/Type/data/assert.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,37 @@ public function assertShape(array $a): void
2929
assertType('array{name: string, age: int, location?: array{city: string, state: string, country: string}}', $b);
3030
}
3131

32+
/**
33+
* @param array<mixed> $a
34+
*/
35+
public function assertNullishShape(array $a): void
36+
{
37+
$specification = Type\shape([
38+
'name' => Type\string(),
39+
'bio' => Type\nullish(Type\string()),
40+
]);
41+
42+
$b = $specification->assert($a);
43+
44+
assertType('array{name: string, bio: string|null}', $a);
45+
assertType('array{name: string, bio: string|null}', $b);
46+
}
47+
48+
/**
49+
* @param array<mixed> $a
50+
*/
51+
public function assertOptionalNullishShape(array $a): void
52+
{
53+
$specification = Type\shape([
54+
'bio' => Type\optional(Type\nullish(Type\string())),
55+
]);
56+
57+
$b = $specification->assert($a);
58+
59+
assertType('array{bio?: string|null}', $a);
60+
assertType('array{bio?: string|null}', $b);
61+
}
62+
3263
public function assertInt($i): void
3364
{
3465
$spec = Type\int();

tests/Type/data/coerce.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,35 @@ public function coerceShape(array $input): void
2929
assertType('array<mixed>', $input);
3030
}
3131

32+
/**
33+
* @param array<mixed> $input
34+
*/
35+
public function coerceNullishShape(array $input): void
36+
{
37+
$specification = Type\shape([
38+
'name' => Type\string(),
39+
'bio' => Type\nullish(Type\string()),
40+
]);
41+
42+
$output = $specification->coerce($input);
43+
44+
assertType('array{name: string, bio: string|null}', $output);
45+
}
46+
47+
/**
48+
* @param array<mixed> $input
49+
*/
50+
public function coerceOptionalNullishShape(array $input): void
51+
{
52+
$specification = Type\shape([
53+
'bio' => Type\optional(Type\nullish(Type\string())),
54+
]);
55+
56+
$output = $specification->coerce($input);
57+
58+
assertType('array{bio?: string|null}', $output);
59+
}
60+
3261
public function coerceInt($i): void
3362
{
3463
$spec = Type\int();

tests/Type/data/matches.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,39 @@ public function matchesShape(array $a): void
3030
}
3131
}
3232

33+
/**
34+
* @param array<mixed> $a
35+
*/
36+
public function matchesNullishShape(array $a): void
37+
{
38+
$specification = Type\shape([
39+
'name' => Type\string(),
40+
'bio' => Type\nullish(Type\string()),
41+
]);
42+
43+
if ($specification->matches($a)) {
44+
assertType('array{name: string, bio: string|null}', $a);
45+
} else {
46+
assertType('array<mixed>', $a);
47+
}
48+
}
49+
50+
/**
51+
* @param array<mixed> $a
52+
*/
53+
public function matchesOptionalNullishShape(array $a): void
54+
{
55+
$specification = Type\shape([
56+
'bio' => Type\optional(Type\nullish(Type\string())),
57+
]);
58+
59+
if ($specification->matches($a)) {
60+
assertType('array{bio?: string|null}', $a);
61+
} else {
62+
assertType('non-empty-array<mixed>', $a);
63+
}
64+
}
65+
3366
public function matchesInt($i): void
3467
{
3568
$spec = Type\int();

0 commit comments

Comments
 (0)