The trick, as identified by Tom Nowell, is to add a reference to global $post.
When you run the_query->the_post(), WordPress loads the first result of the query into a global $post object. This is how it sets things up for all of the regular template tags:
the_title()
the_content()
the_excerpt()
- etc ...
You see that we don't pass anything in to these functions. We just call them. Each of these functions will internally reference the global $post object in order to parse, prepare, and print the desired output.
Inside your loop, you call the_post() just fine to populate the data, but you don't have a reference to the data itself within the scope of your loop. If you wanted to avoid referencing the global $post object, you could instead use get_the_ID().
Like the other template tags I've referenced above, get_the_ID() summons data from the global $post object internally, so you don't have to do it yourself.
But if you do want to do this yourself, just add a global reference before you try to use $post:
$the_query = new WP_Query( array(
'post_type' => 'custompost',
) );
while ( $the_query->have_posts() ) : $the_query->the_post();
global $post; // Add this line and you're golden
echo $post->ID;
endwhile;
What is wp_reset_postdata()?
If you're building multiple loops, (i.e. you have a large post loop, but call some secondary loop inside of it), you can call wp_reset_postdata() to reset things.
Basically, the_post() will set the global $post object to have the data for the requested query. the_query->the_post() will overwrite $post with data from the_query. wp_reset_postdata() will reset $post to the original query.
So if you're using nested or multiple loops, wp_reset_postdata() is a way to get back to the loop and $post object you had available before you called your secondary the_query->the_post().