diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md
index 530b51c4..989292da 100644
--- a/docs/rector_rules_overview.md
+++ b/docs/rector_rules_overview.md
@@ -1326,6 +1326,26 @@ Change if throw to throw_if
+## TypeHintTappableCallRector
+
+Automatically type hints your tappable closures
+
+- class: [`RectorLaravel\Rector\FuncCall\TypeHintTappableCallRector`](../src/Rector/FuncCall/TypeHintTappableCallRector.php)
+
+```diff
+-tap($collection, function ($collection) {}
++tap($collection, function (Collection $collection) {}
+```
+
+
+
+```diff
+-(new Collection)->tap(function ($collection) {}
++(new Collection)->tap(function (Collection $collection) {}
+```
+
+
+
## UnifyModelDatesWithCastsRector
Unify Model `$dates` property with `$casts`
diff --git a/src/Rector/FuncCall/TypeHintTappableCallRector.php b/src/Rector/FuncCall/TypeHintTappableCallRector.php
new file mode 100644
index 00000000..3c7a2e92
--- /dev/null
+++ b/src/Rector/FuncCall/TypeHintTappableCallRector.php
@@ -0,0 +1,134 @@
+tap(function ($collection) {}
+CODE_SAMPLE,
+ <<<'CODE_SAMPLE'
+(new Collection)->tap(function (Collection $collection) {}
+CODE_SAMPLE
+ ),
+ ]
+ );
+ }
+
+ public function getNodeTypes(): array
+ {
+ return [MethodCall::class, FuncCall::class];
+ }
+
+ /**
+ * @param MethodCall|FuncCall $node
+ */
+ public function refactor(Node $node): ?Node
+ {
+ if (! $this->isName($node->name, 'tap')) {
+ return null;
+ }
+
+ if ($node->isFirstClassCallable()) {
+ return null;
+ }
+
+ if ($node instanceof MethodCall && $node->getArgs() !== []) {
+ return $this->refactorMethodCall($node);
+ }
+
+ if (count($node->getArgs()) < 2 || ! $node->getArgs()[1]->value instanceof Closure) {
+ return null;
+ }
+
+ /** @var Closure $closure */
+ $closure = $node->getArgs()[1]->value;
+
+ if ($closure->getParams() === []) {
+ return null;
+ }
+
+ $this->refactorParameter($closure->getParams()[0], $node->getArgs()[0]->value);
+
+ return $node;
+ }
+
+ private function refactorParameter(Param $param, Node $node): void
+ {
+ $nodePhpStanType = $this->nodeTypeResolver->getType($node);
+
+ // already set → no change
+ if ($param->type instanceof Node) {
+ $currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type);
+ if ($this->typeComparator->areTypesEqual($currentParamType, $nodePhpStanType)) {
+ return;
+ }
+ }
+
+ $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($nodePhpStanType, TypeKind::PARAM);
+ $param->type = $paramTypeNode;
+ }
+
+ private function refactorMethodCall(MethodCall $methodCall): ?MethodCall
+ {
+ if (! $this->isTappableCall($methodCall)) {
+ return null;
+ }
+
+ if (! $methodCall->getArgs()[0]->value instanceof Closure) {
+ return null;
+ }
+
+ /** @var Closure $closure */
+ $closure = $methodCall->getArgs()[0]->value;
+
+ if ($closure->getParams() === []) {
+ return null;
+ }
+
+ $this->refactorParameter($closure->getParams()[0], $methodCall->var);
+
+ return $methodCall;
+ }
+
+ private function isTappableCall(MethodCall $methodCall): bool
+ {
+ return $this->isObjectType($methodCall->var, new ObjectType(self::TAPPABLE_TRAIT));
+ }
+}
diff --git a/stubs/Illuminate/Support/Traits/Tappable.php b/stubs/Illuminate/Support/Traits/Tappable.php
new file mode 100644
index 00000000..e70c2934
--- /dev/null
+++ b/stubs/Illuminate/Support/Traits/Tappable.php
@@ -0,0 +1,14 @@
+tap(function ($example) {
+
+});
+
+?>
+-----
+tap(function (\RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample $example) {
+
+});
+
+?>
diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_args.php.inc b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_args.php.inc
new file mode 100644
index 00000000..e23bdf9f
--- /dev/null
+++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_args.php.inc
@@ -0,0 +1,13 @@
+tap();
+
+?>
diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_closure_parameter.php.inc b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_closure_parameter.php.inc
new file mode 100644
index 00000000..15c180c6
--- /dev/null
+++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_closure_parameter.php.inc
@@ -0,0 +1,15 @@
+tap(function () {
+
+});
+
+?>
diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tap_calls.php.inc b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tap_calls.php.inc
new file mode 100644
index 00000000..00df2fb6
--- /dev/null
+++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tap_calls.php.inc
@@ -0,0 +1,7 @@
+
diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tappable_object.php.inc b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tappable_object.php.inc
new file mode 100644
index 00000000..58b21b9f
--- /dev/null
+++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tappable_object.php.inc
@@ -0,0 +1,13 @@
+tap(function ($example) {
+
+});
+
+?>
diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/Source/NonTappableExample.php b/tests/Rector/FuncCall/TypeHintTappableCallRector/Source/NonTappableExample.php
new file mode 100644
index 00000000..94860e79
--- /dev/null
+++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/Source/NonTappableExample.php
@@ -0,0 +1,7 @@
+doTestFile($filePath);
+ }
+
+ public function provideConfigFilePath(): string
+ {
+ return __DIR__ . '/config/configured_rule.php';
+ }
+}
diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/config/configured_rule.php b/tests/Rector/FuncCall/TypeHintTappableCallRector/config/configured_rule.php
new file mode 100644
index 00000000..99084998
--- /dev/null
+++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/config/configured_rule.php
@@ -0,0 +1,12 @@
+import(__DIR__ . '/../../../../../config/config.php');
+
+ $rectorConfig->rule(TypeHintTappableCallRector::class);
+};