Shouldn’t I be able to modify the main query by this filter?

The reason your code isn’t working is because by the time archive.php runs, the pre_get_posts hook has already fired.

You can easily accomplish the task in your functions.php file:

add_action( 'pre_get_posts', 'wpse_pre_get_posts' );
function wpse_pre_get_posts( $query ) {
  remove_action( 'pre_get_posts', 'wpse_pre_get_posts' );
  //* $query->have_posts() will always return false from this hook
  //* So get the posts with the query to check if they're empty
  $posts = get_posts( $query->query );
  if( empty( $posts ) && $query->is_main_query() ) {
    $query->set( 'post_type', 'article' );
  }
}

And then your archive.php would be a simple loop:

if( have_posts() ) :
  while( have_posts() ) :
    the_post(); 
    the_title();
  endwhile;
endif;

This will run the query twice per request, which I wouldn’t do, but offhand I can’t see another way to do what you want.

Thanks @Milo. Edited because at the pre_get_posts hook, the query hasn’t run yet. Duh.