diff --git a/lib/graphql/execution/interpreter/runtime.rb b/lib/graphql/execution/interpreter/runtime.rb
index 44f0d904a4..ce8908b107 100644
--- a/lib/graphql/execution/interpreter/runtime.rb
+++ b/lib/graphql/execution/interpreter/runtime.rb
@@ -239,8 +239,7 @@ def evaluate_selection_with_args(kwarg_arguments, field_defn, next_path, ast_nod
 
                   extra_args[:lookahead] = Execution::Lookahead.new(
                     query: query,
-                    ast_nodes: field_ast_nodes,
-                    field: field_defn,
+                    selections_by_type: { owner_type => field_ast_nodes}
                   )
                 when :argument_details
                   # Use this flag to tell Interpreter::Arguments to add itself
diff --git a/lib/graphql/execution/lookahead.rb b/lib/graphql/execution/lookahead.rb
index abd940c921..e37ed7d9db 100644
--- a/lib/graphql/execution/lookahead.rb
+++ b/lib/graphql/execution/lookahead.rb
@@ -28,34 +28,47 @@ module Execution
     #   end
     class Lookahead
       # @param query [GraphQL::Query]
-      # @param ast_nodes [Array<GraphQL::Language::Nodes::Field>, Array<GraphQL::Language::Nodes::OperationDefinition>]
       # @param field [GraphQL::Schema::Field] if `ast_nodes` are fields, this is the field definition matching those nodes
       # @param root_type [Class] if `ast_nodes` are operation definition, this is the root type for that operation
-      def initialize(query:, ast_nodes:, field: nil, root_type: nil, owner_type: nil)
-        @ast_nodes = ast_nodes.freeze
-        @field = field
+      def initialize(query:, selections_by_type:, root_type: nil)
+        @selections_by_type = selections_by_type
         @root_type = root_type
         @query = query
         @selected_type = @field ? @field.type.unwrap : root_type
-        @owner_type = owner_type
       end
 
       # @return [Array<GraphQL::Language::Nodes::Field>]
-      attr_reader :ast_nodes
+      def ast_nodes
+        @ast_nodes ||= @selections_by_type.values.flatten
+      end
 
       # @return [GraphQL::Schema::Field]
-      attr_reader :field
+      def field
+        fields.first
+      end
+
+      def fields
+        @fields ||= @selections_by_type.map do |t, ast_nodes|
+          get_class_based_field(t, ast_nodes.first.name)
+        end
+      end
 
       # @return [GraphQL::Schema::Object, GraphQL::Schema::Union, GraphQL::Schema::Interface]
-      attr_reader :owner_type
+      def owner_type
+        owner_types.first
+      end
+
+      def owner_types
+        @owner_types ||= @selections_by_type.keys
+      end
 
       # @return [Hash<Symbol, Object>]
       def arguments
         if defined?(@arguments)
           @arguments
         else
-          @arguments = if @field
-            @query.schema.after_lazy(@query.arguments_for(@ast_nodes.first, @field)) do |args|
+          @arguments = if (f = field)
+            @query.schema.after_lazy(@query.arguments_for(ast_nodes.first, f)) do |args|
               args.is_a?(Execution::Interpreter::Arguments) ? args.keyword_arguments : args
             end
           else
@@ -88,23 +101,27 @@ def selected?
       # Like {#selects?}, but can be used for chaining.
       # It returns a null object (check with {#selected?})
       # @return [GraphQL::Execution::Lookahead]
-      def selection(field_name, selected_type: @selected_type, arguments: nil)
+      def selection(field_name, selected_type: nil, arguments: nil)
         next_field_name = normalize_name(field_name)
+        subselections_by_type = {}
 
-        next_field_defn = get_class_based_field(selected_type, next_field_name)
-        if next_field_defn
-          next_nodes = []
-          @ast_nodes.each do |ast_node|
+        @selections_by_type.each do |owner_type, ast_nodes|
+          next if selected_type && owner_type != selected_type
+          subselection_owner_type = if @root_type
+            @root_type
+          else
+            field_for_node = get_class_based_field(owner_type, ast_nodes.first.name)
+            field_for_node.type.unwrap
+          end
+          ast_nodes.each do |ast_node|
             ast_node.selections.each do |selection|
-              find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
+              find_selected_nodes(selection, next_field_name, subselection_owner_type, arguments: arguments, matches: subselections_by_type)
             end
           end
+        end
 
-          if next_nodes.any?
-            Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
-          else
-            NULL_LOOKAHEAD
-          end
+        if subselections_by_type.any?
+          Lookahead.new(query: @query, selections_by_type: subselections_by_type)
         else
           NULL_LOOKAHEAD
         end
@@ -127,23 +144,27 @@ def selection(field_name, selected_type: @selected_type, arguments: nil)
       # @return [Array<GraphQL::Execution::Lookahead>]
       def selections(arguments: nil)
         subselections_by_type = {}
-        subselections_on_type = subselections_by_type[@selected_type] = {}
 
-        @ast_nodes.each do |node|
-          find_selections(subselections_by_type, subselections_on_type, @selected_type, node.selections, arguments)
+        @selections_by_type.each do |owner_type, ast_nodes|
+          next_field_type = if @root_type
+            @root_type
+          else
+            next_field = get_class_based_field(owner_type, ast_nodes.first.name)
+            next_field.type.unwrap
+          end
+          ast_nodes.each do |node|
+            find_selections(subselections_by_type, next_field_type, node.selections, arguments)
+          end
         end
 
-        subselections = []
-
+        lookaheads = []
         subselections_by_type.each do |type, ast_nodes_by_response_key|
           ast_nodes_by_response_key.each do |response_key, ast_nodes|
-            field_defn = get_class_based_field(type, ast_nodes.first.name)
-            lookahead = Lookahead.new(query: @query, ast_nodes: ast_nodes, field: field_defn, owner_type: type)
-            subselections.push(lookahead)
+            lookaheads << Lookahead.new(query: @query, selections_by_type: {type => ast_nodes})
           end
         end
 
-        subselections
+        lookaheads
       end
 
       # The method name of the field.
@@ -157,11 +178,11 @@ def selections(arguments: nil)
       #
       # @return [Symbol]
       def name
-        @field && @field.original_name
+        field && field.original_name
       end
 
       def inspect
-        "#<GraphQL::Execution::Lookahead #{@field ? "@field=#{@field.path.inspect}": "@root_type=#{@root_type}"} @ast_nodes.size=#{@ast_nodes.size}>"
+        "#<GraphQL::Execution::Lookahead #{field ? "field=#{field.path.inspect}": "@root_type=#{@root_type}"} ast_nodes.size=#{ast_nodes.size}>"
       end
 
       # This is returned for {Lookahead#selection} when a non-existent field is passed
@@ -232,63 +253,78 @@ def skipped_by_directive?(ast_selection)
         false
       end
 
-      def find_selections(subselections_by_type, selections_on_type, selected_type, ast_selections, arguments)
+      def add_found_selection(subselections_by_type, selected_type, response_key, result)
+        type_selections = subselections_by_type[selected_type] ||= {}
+        results = type_selections[response_key] ||= []
+        results << result
+        nil
+      end
+
+      def find_selections(subselections_by_type, selected_type, ast_selections, arguments)
         ast_selections.each do |ast_selection|
           next if skipped_by_directive?(ast_selection)
 
           case ast_selection
           when GraphQL::Language::Nodes::Field
             response_key = ast_selection.alias || ast_selection.name
-            if selections_on_type.key?(response_key)
-              selections_on_type[response_key] << ast_selection
-            elsif arguments.nil? || arguments.empty?
-              selections_on_type[response_key] = [ast_selection]
+            if arguments.nil? || arguments.empty?
+              add_found_selection(subselections_by_type, selected_type, response_key, ast_selection)
             else
               field_defn = get_class_based_field(selected_type, ast_selection.name)
               if arguments_match?(arguments, field_defn, ast_selection)
-                selections_on_type[response_key] = [ast_selection]
+                add_found_selection(subselections_by_type, selected_type, response_key, ast_selection)
               end
             end
           when GraphQL::Language::Nodes::InlineFragment
             on_type = selected_type
-            subselections_on_type = selections_on_type
             if (t = ast_selection.type)
               # Assuming this is valid, that `t` will be found.
               on_type = @query.schema.get_type(t.name).type_class
-              subselections_on_type = subselections_by_type[on_type] ||= {}
             end
-            find_selections(subselections_by_type, subselections_on_type, on_type, ast_selection.selections, arguments)
+            find_selections(subselections_by_type, on_type, ast_selection.selections, arguments)
           when GraphQL::Language::Nodes::FragmentSpread
             frag_defn = @query.fragments[ast_selection.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{ast_selection.name} (found: #{@query.fragments.keys})")
             # Again, assuming a valid AST
             on_type = @query.schema.get_type(frag_defn.type.name).type_class
-            subselections_on_type = subselections_by_type[on_type] ||= {}
-            find_selections(subselections_by_type, subselections_on_type, on_type, frag_defn.selections, arguments)
+            find_selections(subselections_by_type, on_type, frag_defn.selections, arguments)
           else
             raise "Invariant: Unexpected selection type: #{ast_selection.class}"
           end
         end
       end
 
-      # If a selection on `node` matches `field_name` (which is backed by `field_defn`)
+      # If a selection on `node` matches `field_name`
       # and matches the `arguments:` constraints, then add that node to `matches`
-      def find_selected_nodes(node, field_name, field_defn, arguments:, matches:)
+      def find_selected_nodes(node, field_name, owner_type, arguments:, matches:)
         return if skipped_by_directive?(node)
         case node
         when GraphQL::Language::Nodes::Field
           if node.name == field_name
-            if arguments.nil? || arguments.empty?
+            field_defn = get_class_based_field(owner_type, field_name)
+            if field_defn.nil?
+              # This is a buggy query, do nothing
+            elsif arguments.nil? || arguments.empty?
               # No constraint applied
-              matches << node
+              results = matches[owner_type] ||= []
+              results << node
             elsif arguments_match?(arguments, field_defn, node)
-              matches << node
+              results = matches[owner_type] ||= []
+              results << node
             end
           end
         when GraphQL::Language::Nodes::InlineFragment
-          node.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches) }
+          new_owner_type = if (t = node.type)
+            # Assuming this is valid, that `t` will be found.
+            @query.schema.get_type(t.name).type_class
+          else
+            owner_type
+          end
+          node.selections.each { |s| find_selected_nodes(s, field_name, new_owner_type, arguments: arguments, matches: matches) }
         when GraphQL::Language::Nodes::FragmentSpread
           frag_defn = @query.fragments[node.name] || raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})")
-          frag_defn.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches) }
+          # Assuming this is valid
+          new_owner_type = @query.schema.get_type(frag_defn.type.name).type_class
+          frag_defn.selections.each { |s| find_selected_nodes(s, field_name, new_owner_type, arguments: arguments, matches: matches) }
         else
           raise "Unexpected selection comparison on #{node.class.name} (#{node})"
         end
diff --git a/lib/graphql/query.rb b/lib/graphql/query.rb
index e70b20fe50..0e441eaf98 100644
--- a/lib/graphql/query.rb
+++ b/lib/graphql/query.rb
@@ -166,7 +166,7 @@ def lookahead
         ast_node = selected_operation
         root_type = warden.root_type_for_operation(ast_node.operation_type || "query")
         root_type = root_type.type_class || raise("Invariant: `lookahead` only works with class-based types")
-        GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, ast_nodes: [ast_node])
+        GraphQL::Execution::Lookahead.new(query: self, root_type: root_type, selections_by_type: { root_type => [ast_node] })
       end
     end
 
diff --git a/lib/graphql/query/context.rb b/lib/graphql/query/context.rb
index a4bc8fe16a..bf041e9447 100644
--- a/lib/graphql/query/context.rb
+++ b/lib/graphql/query/context.rb
@@ -76,7 +76,7 @@ def execution_errors
         def lookahead
           ast_nodes = irep_node.ast_nodes
           field = irep_node.definition.metadata[:type_class] || raise("Lookahead is only compatible with class-based schemas")
-          Execution::Lookahead.new(query: query, ast_nodes: ast_nodes, field: field)
+          Execution::Lookahead.new(query: query, selections_by_type: { field.owner => ast_nodes })
         end
       end
 
diff --git a/spec/graphql/execution/lookahead_spec.rb b/spec/graphql/execution/lookahead_spec.rb
index a91ee809e3..45a5a37465 100644
--- a/spec/graphql/execution/lookahead_spec.rb
+++ b/spec/graphql/execution/lookahead_spec.rb
@@ -85,6 +85,13 @@ class Schema < GraphQL::Schema
     end
   end
 
+  let(:first_lookahead) {
+    ast_node = document.definitions.first.selections.first
+    type = LookaheadTest::Query
+    lookahead = GraphQL::Execution::Lookahead.new(query: query, selections_by_type: { type => [ast_node] })
+  }
+
+
   describe "looking ahead" do
     let(:document) {
       GraphQL.parse <<-GRAPHQL
@@ -138,7 +145,7 @@ class Schema < GraphQL::Schema
         GRAPHQL
       }
 
-      it "finds fields on object types and interface types" do
+      it "enumerates fields on object types and interface types" do
         node_lookahead = query.lookahead.selection("node")
         assert_equal [:id, :name, :latin_name], node_lookahead.selections.map(&:name)
       end
@@ -150,7 +157,7 @@ class Schema < GraphQL::Schema
       end
 
       it "works for field lookaheads" do
-        assert_includes query.lookahead.selection(:find_bird_species).inspect, "#<GraphQL::Execution::Lookahead @field="
+        assert_includes query.lookahead.selection(:find_bird_species).inspect, "#<GraphQL::Execution::Lookahead field=\"Query.findBirdSpecies\""
       end
     end
 
@@ -238,7 +245,11 @@ class Schema < GraphQL::Schema
       }
 
       it "finds selections using merging" do
-        merged_lookahead = query.lookahead.selection(:find_bird_species).selection(:similar_species)
+        merged_lookahead = query
+          .lookahead
+          .selection(:find_bird_species)
+          .selection(:similar_species)
+
         assert merged_lookahead.selects?(:__typename)
         assert merged_lookahead.selects?(:is_waterfowl)
         assert merged_lookahead.selects?(:name)
@@ -299,26 +310,17 @@ def query(doc = document)
     end
 
     it "provides a list of all selections" do
-      ast_node = document.definitions.first.selections.first
-      field = LookaheadTest::Query.fields["findBirdSpecies"]
-      lookahead = GraphQL::Execution::Lookahead.new(query: query, ast_nodes: [ast_node], field: field)
-      assert_equal [:name, :similar_species], lookahead.selections.map(&:name)
+      assert_equal [:name, :similar_species], first_lookahead.selections.map(&:name)
     end
 
     it "filters outs selections which do not match arguments" do
-      ast_node = document.definitions.first
-      lookahead = GraphQL::Execution::Lookahead.new(query: query, ast_nodes: [ast_node], root_type: LookaheadTest::Query)
       arguments = { by_name: "Cardinal" }
-
-      assert_equal lookahead.selections(arguments: arguments).map(&:name), []
+      assert_equal query.lookahead.selections(arguments: arguments).map(&:name), []
     end
 
     it "includes selections which match arguments" do
-      ast_node = document.definitions.first
-      lookahead = GraphQL::Execution::Lookahead.new(query: query, ast_nodes: [ast_node], root_type: LookaheadTest::Query)
       arguments = { by_name: "Laughing Gull" }
-
-      assert_equal lookahead.selections(arguments: arguments).map(&:name), [:find_bird_species]
+      assert_equal query.lookahead.selections(arguments: arguments).map(&:name), [:find_bird_species]
     end
 
     it 'handles duplicate selections across fragments' do
@@ -371,16 +373,13 @@ def query(doc = document)
 
       node_lookahead = query.lookahead.selection("node")
       assert_equal(
-        [[LookaheadTest::Node, :id], [LookaheadTest::BirdSpecies, :name], [LookaheadTest::BirdGenus, :name]],
+        [[LookaheadTest::BirdSpecies, :name], [LookaheadTest::BirdGenus, :name], [LookaheadTest::Node, :id]],
         node_lookahead.selections.map { |s| [s.owner_type, s.name] }
       )
     end
 
     it "works for missing selections" do
-      ast_node = document.definitions.first.selections.first
-      field = LookaheadTest::Query.fields["findBirdSpecies"]
-      lookahead = GraphQL::Execution::Lookahead.new(query: query, ast_nodes: [ast_node], field: field)
-      null_lookahead = lookahead.selection(:genus)
+      null_lookahead = first_lookahead.selection(:genus)
       # This is an implementation detail, but I want to make sure the test is set up right
       assert_instance_of GraphQL::Execution::Lookahead::NullLookahead, null_lookahead
       assert_equal [], null_lookahead.selections
@@ -400,13 +399,178 @@ def query(doc = document)
         variables: { skipName: false, includeGenus: true })
       lookahead = query.lookahead.selection("findBirdSpecies")
       assert_equal [:id, :name, :genus], lookahead.selections.map(&:name)
-      assert_equal true, lookahead.selects?(:name)
 
       query = GraphQL::Query.new(LookaheadTest::Schema, document: document,
         variables: { skipName: true, includeGenus: false })
       lookahead = query.lookahead.selection("findBirdSpecies")
       assert_equal [:id], lookahead.selections.map(&:name)
+    end
+  end
+
+  describe '#selects?' do
+    let(:document) {
+      GraphQL.parse <<-GRAPHQL
+        query {
+          findBirdSpecies(byName: "Laughing Gull") {
+            name
+            similarSpecies {
+              likesWater: isWaterfowl
+            }
+          }
+        }
+      GRAPHQL
+    }
+
+    def query(doc = document)
+      GraphQL::Query.new(LookaheadTest::Schema, document: doc)
+    end
+
+    it "returns true for a field that is selected" do
+      assert first_lookahead.selects?(:name)
+      assert first_lookahead.selects?(:similar_species)
+      assert_equal false, first_lookahead.selects?(:is_waterfowl)
+    end
+
+    it "returns false for a field that is not selected" do
+      assert_equal false, first_lookahead.selects?(:is_waterfowl)
+    end
+
+    it "returns false for a selection which does not match arguments" do
+      arguments = { by_name: "Cardinal" }
+      assert_equal false, query.lookahead.selects?(:name, arguments: arguments)
+    end
+
+    it "returns true for a selection which matches arguments" do
+      arguments = { by_name: "Laughing Gull" }
+      assert query.lookahead.selects?(:find_bird_species, arguments: arguments)
+    end
+
+    it 'returns true for selection that is duplicated across fragments' do
+      doc = GraphQL.parse <<-GRAPHQL
+        query {
+          ... on Query {
+            ...MoreFields
+          }
+        }
+
+        fragment MoreFields on Query {
+          findBirdSpecies(byName: "Laughing Gull") {
+            name
+          }
+          findBirdSpecies(byName: "Laughing Gull") {
+            ...EvenMoreFields
+          }
+        }
+
+        fragment EvenMoreFields on BirdSpecies {
+          similarSpecies {
+            likesWater: isWaterfowl
+          }
+        }
+      GRAPHQL
+
+      lookahead = query(doc).lookahead
+      assert lookahead.selects?(:find_bird_species)
+
+      assert lookahead.selection(:find_bird_species).selects?(:name)
+      assert lookahead.selection(:find_bird_species).selects?(:similar_species)
+    end
+
+    it "returns true for a field name that exists on multiple distinct types" do
+      query = GraphQL::Query.new(LookaheadTest::Schema, <<-GRAPHQL)
+        query {
+          node(id: "Cardinal") {
+            ... on BirdSpecies {
+              name
+              similarSpecies {
+                name
+              }
+            }
+            ... on BirdGenus {
+              name
+            }
+            id
+          }
+        }
+      GRAPHQL
+
+      node_lookahead = query.lookahead.selection("node")
+      assert node_lookahead.selects?(:id)
+      assert node_lookahead.selects?(:name)
+      assert node_lookahead.selection(:similar_species).selects?(:name)
+    end
+
+    it "returns false on missing selections" do
+      assert_equal false, first_lookahead.selection(:genus).selects?(:name)
+    end
+
+    it "returns true for fields enabled by directives" do
+      document = GraphQL.parse <<-GRAPHQL
+        query($skipName: Boolean!, $includeGenus: Boolean!){
+          findBirdSpecies(byName: "Cardinal") {
+            id
+            name @skip(if: $skipName)
+            genus @include(if: $includeGenus)
+          }
+        }
+      GRAPHQL
+      query = GraphQL::Query.new(LookaheadTest::Schema, document: document,
+        variables: { skipName: false, includeGenus: true })
+      lookahead = query.lookahead.selection("findBirdSpecies")
+      assert lookahead.selects?(:name)
+      assert lookahead.selects?(:genus)
+    end
+
+    it "returns false for fields disabled by directive" do
+      document = GraphQL.parse <<-GRAPHQL
+        query($skipName: Boolean!, $includeGenus: Boolean!){
+          findBirdSpecies(byName: "Cardinal") {
+            id
+            name @skip(if: $skipName)
+            genus @include(if: $includeGenus)
+          }
+        }
+      GRAPHQL
+      query = GraphQL::Query.new(LookaheadTest::Schema, document: document,
+        variables: { skipName: true, includeGenus: false })
+      lookahead = query.lookahead.selection("findBirdSpecies")
+      assert lookahead.selects?(:id)
       assert_equal false, lookahead.selects?(:name)
+      assert_equal false, lookahead.selects?(:genus)
+    end
+
+    describe "fields on interfaces" do
+      let(:document) {
+        GraphQL.parse <<-GRAPHQL
+        query {
+          node(id: "Cardinal") {
+            id
+            ... on BirdSpecies {
+              name
+            }
+            ...Other
+          }
+        }
+        fragment Other on BirdGenus {
+          latinName
+        }
+        GRAPHQL
+      }
+
+      it "returns true for fields on direct object types" do
+        node_lookahead = query.lookahead.selection("node")
+        assert node_lookahead.selects?(:id)
+      end
+
+      it "returns true for fields on interface types" do
+        node_lookahead = query.lookahead.selection("node")
+        assert node_lookahead.selects?(:name)
+      end
+
+      it "returns true for fields on interface types through fragments" do
+        node_lookahead = query.lookahead.selection("node")
+        assert node_lookahead.selects?(:latin_name)
+      end
     end
   end
 end