The so-called "empty object type" {} is very wide; almost all values will be assignable to it. Roughly, it will accept any value that can be indexed into like an object. This includes all non-primitive types like object, but it also includes the five primitive types with wrapper objects: so string, number, boolean, bigint, and symbol are assignable to {} because values of these types are automatically wrapped in String, Number, Boolean, BigInt, and Symbol objects (respectively) when you access members on them as if they were objects.
In some sense this means your ExternalFoo type that cannot be changed is redundant: {} | string | number is basically equivalent to {} (but is both displayed differently and might be treated differently by the compiler in some circumstances). I'm wondering if the actual intent of ExternalFoo was supposed to be more like object | string | number instead of {} | string | number. Was it really meant to accept boolean, for example?
const wasThisIntended: ExternalFoo = true;
But you said you can't change it, so that's out of scope.
TypeScript does not have negated types (see microsoft/TypeScript#29317) so you cannot take ExternalFoo and quickly produce the version of it that rejects string. Such a type like ExternalFoo & not string is simply not representable in TypeScript as it currently exists. The Exclude utility type only filters unions. And while {} | number | string can be filtered to produce either {} | number, {} itself cannot be filtered because it is not a union.
In order to exclude string from ExternalFoo, we would need not only to remove string from the definition, but change {} to something that also excludes string. The type {} is more or less the same as object | string | number | boolean | bigint | symbol. We can, if we want, remove string from that, to produce
type Foo = object | number | boolean | bigint | symbol;
So let's try that!
const foo1: Foo = {}; // okay
const foo2: Foo = 5; // okay
const foo3: Foo = "bar"; // error!
Looks good. As desired, you can assign {} and 5 to a variable of type Foo, but you cannot assign "bar" to it. This still happens:
const stillHappens: Foo = true; // okay
But boolean is not a string so I guess it's desired also.
For your longer version, I'd suggest trying to convert a generic type T extends ExternalFoo to T & Foo and see if it works for you:
type HasLength = { length: number };
let l: HasLength;
l = [1, 2, 3]; // okay
l = "hello"; // okay
l = { length: 19 }; // okay
type NotString<T extends ExternalFoo> = T & Foo;
type HasLengthNotString = NotString<{ length: number }>;
let n: HasLengthNotString;
n = [1, 2, 3]; // okay
n = "hello"; // error
n = { length: 19 }; // okay
Playground link to code