I kind of feel guilty answering my own bounty question, so I'm going to mark it as community. The reason I'm writing my own is because I feel like the other answer really buries the lead. I had to perform hours and hours of my own research after reading it to be able to write this. That being the case, I think my answer will be more helpful to people that start in my boat, where I didn't know what I didn't know. I also think there's an additional solution to the problem, though it would be more invasive.
My original question was, "If the first version is invalid, why doesn't typescript inform me?" Here is the other answer's explanation:
Because you have enabled esModuleInterop which also enables allowSyntheticDefaultImports. The CommonJS bundle is actually incompatible with that option but TypeScript doesn't know.
This is absolutely true, but when it comes to understanding what's going on, it's the tip of the iceberg:
If you look at the reference documentation, it recommends you set esModuleInterop to true. Why would it make that recommendation if it reduces type safety? Well, that is not why it recommends you set it to true. In fact, this setting does not reduce type safety -- it increases it by fixing some legacy typescript bugs, specifically two that deal with how typescript handles requires. You can read the documentation for more details on that, but in my opinion, if you are using node libraries, I think it's a good idea to set esModuleInterop to true.
But! esModuleInterop has a side effect. At the very bottom of its documentation, it says:
Enabling esModuleInterop will also enable allowSyntheticDefaultImports.
Err... kinda. IMO, this documentation is incorrect. What it should really say is, "Enabling esModuleInterop will default allowSyntheticDefaultImports to true." If you look at the allowSyntheticDefaultImports documentation, it says this on the right-hand side:

Hey, notice how in that upper right-hand corner it doesn't say this setting is recommended? That's probably because this setting reduces type safety: it allows you to type import React from "react"; instead of import * as React from "react"; when the module does not explicitly specify a default export.
Normally (i.e. with allowSyntheticDefaultImports set to false), this would be an error... because it is: you shouldn't be able to default import a module unless it has a default export. Setting this to true makes the compiler say, "Nah, this is fine."
But, when you set allowSyntheticDefaultImports to true, "this flag does not affect the JavaScript emitted by TypeScript." What this means is, this flag lets you pretend the library was written one way at compile time even though it wasn't. At runtime, this will error. Why does the setting even exist? I have no idea, but it probably has to do with historical reasons:
This option brings the behavior of TypeScript in-line with Babel, where extra code is emitted to make using a default export of a module more ergonomic.
It seems like there was(/is?) a point in time where everybody was just assumed to be using Babel. I am not doing that, so the "ergonomic" benefit becomes a runtime error.
As a cleaner method, you should import uuid with import { v4 } from 'uuid';
True, but I think it would also be a good idea to explicitly set allowSyntheticDefaultImports to false. It gives you more type safety. Not only that, it makes import uuid from "uuid"; a compile time error (which it should be).
There is one more thing I don't understand:
Setting allowSyntheticDefaultImports to false also makes imports like import os from "os"; and import _ from "lodash"; compile time errors. But those always ran fine when allowSyntheticDefaultImports was true. There must be some piece I'm missing that explains why those work but uuid doesn't.
I can't find the source of os in my node_modules, but I can look at lodash, and its index.js does this:
module.exports = require('./lodash');
In that required file, it says this at the bottom:
...
/*--------------------------------------------------------------------------*/
// Export lodash.
var _ = runInContext();
// Some AMD build optimizers, like r.js, check for condition patterns like:
if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
// Expose Lodash on the global object to prevent errors when Lodash is
// loaded by a script tag in the presence of an AMD loader.
// See http://requirejs.org/docs/errors.html#mismatch for more details.
// Use `_.noConflict` to remove Lodash from the global object.
root._ = _;
// Define as an anonymous module so, through path mapping, it can be
// referenced as the "underscore" module.
define(function() {
return _;
});
}
// Check for `exports` after `define` in case a build optimizer adds it.
else if (freeModule) {
// Export for Node.js.
(freeModule.exports = _)._ = _;
// Export for CommonJS support.
freeExports._ = _;
}
else {
// Export to the global object.
root._ = _;
}
I don't really understand what all of this is doing, but I think this is defining a global variable named _ at runtime? I guess that means, from a typescript perspective, this coincidentally works out. The type declaration files don't have a default, which would normally cause a runtime error, but almost coincidentally, it all works out in the end because the lodash javascript defines a global _? shrug Maybe this is a pattern that os uses, too, but I've already spent enough hours researching this, so I will leave that for another day/question.