switch_to_blog() not functioning while querying post_types, Why?

There appear to be a couple things at work here:

  1. The switch_to_blog() call does not switch plugins (see ticket #14941), so if your custom post types are defined in a plugin (as they most likely are and should be), then they won’t be loaded in a switch_to_blog() scenario. (This is highly unlikely to changed, judging by the commentary on the ticket.)

  2. Also, the get_post_types() uses a global variable; it doesn’t grab the CPTs registered in the site you switch to, but the list from the site you started from. Global variables don’t seem to be updated on switch_to_blog(), presumably for reasons similar to the reasons plugins aren’t unloaded/loaded on switch_to_blog().

I don’t know that there’s going to be an easy way to do what you’re trying to do, I’m afraid.

A possible work-around

register_post_type() fires an action—registered_post_type—as it finishes up. You should be able to use this to build a list of custom post types on a given site.

Something like this (very simple, untested code):

<?php

add_action( 'registered_post_type', 'wpse_290292_post_types', 10, 2 );
function wpse_290292_post_types( $post_type_name, $post_type_object ) {
    $post_types = get_option( 'my_prefix_post_types', array() );
    if ( 
        ! isset( $post_types[ $post_type_name ] ) 
        || $post_type_object !== $post_types[ $post_type_name ] 
    ) {
        // Adds the $post_type_object to the array if it's a) not already there or b) changed since it was added.
        $post_types[ $post_type_name ] = $post_type_object;
        update_option( 'my_prefix_post_types', $post_types );
    }
}
?>

This will add any registered post types to the *_options table. You’ll probably want to add in some checks to prevent adding the _builtin post types, etc.

Once this is set up—probably as a network-enabled plugin—you should be able to get a given site’s CPTs using the my_prefix_post_types option (feel free to rename that as needed).