diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index a48160137531c..d2eeadea5279c 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -6087,14 +6087,25 @@ namespace ts {
             function inferFromTypes(source: Type, target: Type) {
                 if (source.flags & TypeFlags.Union && target.flags & TypeFlags.Union ||
                     source.flags & TypeFlags.Intersection && target.flags & TypeFlags.Intersection) {
-                    // Source and target are both unions or both intersections. To improve the quality of
-                    // inferences we first reduce the types by removing constituents that are identically
-                    // matched by a constituent in the other type. For example, when inferring from
-                    // 'string | string[]' to 'string | T', we reduce the types to 'string[]' and 'T'.
-                    const reducedSource = reduceUnionOrIntersectionType(<UnionOrIntersectionType>source, <UnionOrIntersectionType>target);
-                    const reducedTarget = reduceUnionOrIntersectionType(<UnionOrIntersectionType>target, <UnionOrIntersectionType>source);
-                    source = reducedSource;
-                    target = reducedTarget;
+                    // Source and target are both unions or both intersections. First, find each
+                    // target constituent type that has an identically matching source constituent
+                    // type, and for each such target constituent type infer from the type to itself.
+                    // When inferring from a type to itself we effectively find all type parameter
+                    // occurrences within that type and infer themselves as their type arguments.
+                    let matchingTypes: Type[];
+                    for (const t of (<UnionOrIntersectionType>target).types) {
+                        if (typeIdenticalToSomeType(t, (<UnionOrIntersectionType>source).types)) {
+                            (matchingTypes || (matchingTypes = [])).push(t);
+                            inferFromTypes(t, t);
+                        }
+                    }
+                    // Next, to improve the quality of inferences, reduce the source and target types by
+                    // removing the identically matched constituents. For example, when inferring from
+                    // 'string | string[]' to 'string | T' we reduce the types to 'string[]' and 'T'.
+                    if (matchingTypes) {
+                        source = removeTypesFromUnionOrIntersection(<UnionOrIntersectionType>source, matchingTypes);
+                        target = removeTypesFromUnionOrIntersection(<UnionOrIntersectionType>target, matchingTypes);
+                    }
                 }
                 if (target.flags & TypeFlags.TypeParameter) {
                     // If target is a type parameter, make an inference, unless the source type contains
@@ -6256,9 +6267,9 @@ namespace ts {
             }
         }
 
-        function typeIdenticalToSomeType(source: Type, target: UnionOrIntersectionType): boolean {
-            for (const t of target.types) {
-                if (isTypeIdenticalTo(source, t)) {
+        function typeIdenticalToSomeType(type: Type, types: Type[]): boolean {
+            for (const t of types) {
+                if (isTypeIdenticalTo(t, type)) {
                     return true;
                 }
             }
@@ -6266,29 +6277,17 @@ namespace ts {
         }
 
         /**
-         * Return the reduced form of the source type. This type is computed by by removing all source
-         * constituents that have an identical match in the target type.
+         * Return a new union or intersection type computed by removing a given set of types
+         * from a given union or intersection type.
          */
-        function reduceUnionOrIntersectionType(source: UnionOrIntersectionType, target: UnionOrIntersectionType) {
-            let sourceTypes = source.types;
-            let sourceIndex = 0;
-            let modified = false;
-            while (sourceIndex < sourceTypes.length) {
-                if (typeIdenticalToSomeType(sourceTypes[sourceIndex], target)) {
-                    if (!modified) {
-                        sourceTypes = sourceTypes.slice(0);
-                        modified = true;
-                    }
-                    sourceTypes.splice(sourceIndex, 1);
-                }
-                else {
-                    sourceIndex++;
+        function removeTypesFromUnionOrIntersection(type: UnionOrIntersectionType, typesToRemove: Type[]) {
+            const reducedTypes: Type[] = [];
+            for (const t of type.types) {
+                if (!typeIdenticalToSomeType(t, typesToRemove)) {
+                    reducedTypes.push(t);
                 }
             }
-            if (modified) {
-                return source.flags & TypeFlags.Union ? getUnionType(sourceTypes, /*noSubtypeReduction*/ true) : getIntersectionType(sourceTypes);
-            }
-            return source;
+            return type.flags & TypeFlags.Union ? getUnionType(reducedTypes, /*noSubtypeReduction*/ true) : getIntersectionType(reducedTypes);
         }
 
         function getInferenceCandidates(context: InferenceContext, index: number): Type[] {
diff --git a/tests/baselines/reference/recursiveUnionTypeInference.js b/tests/baselines/reference/recursiveUnionTypeInference.js
new file mode 100644
index 0000000000000..580a6bf83e5d0
--- /dev/null
+++ b/tests/baselines/reference/recursiveUnionTypeInference.js
@@ -0,0 +1,14 @@
+//// [recursiveUnionTypeInference.ts]
+interface Foo<T> {
+    x: T;
+}
+
+function bar<T>(x: Foo<T> | string): T {
+    return bar(x);
+}
+
+
+//// [recursiveUnionTypeInference.js]
+function bar(x) {
+    return bar(x);
+}
diff --git a/tests/baselines/reference/recursiveUnionTypeInference.symbols b/tests/baselines/reference/recursiveUnionTypeInference.symbols
new file mode 100644
index 0000000000000..8823357394178
--- /dev/null
+++ b/tests/baselines/reference/recursiveUnionTypeInference.symbols
@@ -0,0 +1,23 @@
+=== tests/cases/compiler/recursiveUnionTypeInference.ts ===
+interface Foo<T> {
+>Foo : Symbol(Foo, Decl(recursiveUnionTypeInference.ts, 0, 0))
+>T : Symbol(T, Decl(recursiveUnionTypeInference.ts, 0, 14))
+
+    x: T;
+>x : Symbol(x, Decl(recursiveUnionTypeInference.ts, 0, 18))
+>T : Symbol(T, Decl(recursiveUnionTypeInference.ts, 0, 14))
+}
+
+function bar<T>(x: Foo<T> | string): T {
+>bar : Symbol(bar, Decl(recursiveUnionTypeInference.ts, 2, 1))
+>T : Symbol(T, Decl(recursiveUnionTypeInference.ts, 4, 13))
+>x : Symbol(x, Decl(recursiveUnionTypeInference.ts, 4, 16))
+>Foo : Symbol(Foo, Decl(recursiveUnionTypeInference.ts, 0, 0))
+>T : Symbol(T, Decl(recursiveUnionTypeInference.ts, 4, 13))
+>T : Symbol(T, Decl(recursiveUnionTypeInference.ts, 4, 13))
+
+    return bar(x);
+>bar : Symbol(bar, Decl(recursiveUnionTypeInference.ts, 2, 1))
+>x : Symbol(x, Decl(recursiveUnionTypeInference.ts, 4, 16))
+}
+
diff --git a/tests/baselines/reference/recursiveUnionTypeInference.types b/tests/baselines/reference/recursiveUnionTypeInference.types
new file mode 100644
index 0000000000000..e5d62a051a539
--- /dev/null
+++ b/tests/baselines/reference/recursiveUnionTypeInference.types
@@ -0,0 +1,24 @@
+=== tests/cases/compiler/recursiveUnionTypeInference.ts ===
+interface Foo<T> {
+>Foo : Foo<T>
+>T : T
+
+    x: T;
+>x : T
+>T : T
+}
+
+function bar<T>(x: Foo<T> | string): T {
+>bar : <T>(x: Foo<T> | string) => T
+>T : T
+>x : Foo<T> | string
+>Foo : Foo<T>
+>T : T
+>T : T
+
+    return bar(x);
+>bar(x) : T
+>bar : <T>(x: Foo<T> | string) => T
+>x : Foo<T> | string
+}
+
diff --git a/tests/cases/compiler/recursiveUnionTypeInference.ts b/tests/cases/compiler/recursiveUnionTypeInference.ts
new file mode 100644
index 0000000000000..4ffc6fcf4a78f
--- /dev/null
+++ b/tests/cases/compiler/recursiveUnionTypeInference.ts
@@ -0,0 +1,7 @@
+interface Foo<T> {
+    x: T;
+}
+
+function bar<T>(x: Foo<T> | string): T {
+    return bar(x);
+}