There are a few ways to get some compile-time checking on this but I think all of them require a little bit of repetition of the ThingOptions name.
One way is to make a KeysFrom<T, U> which takes a type T and a candidate type U, which is only compatible with U if the keys of U are a subset of those of T (meaning that T can have keys that U doesn't have, but U can't have keys that T doesn't have):
type KeysFrom<T, U> = { [K in keyof U]: K extends keyof T ? U[K] : never }
So KeysFrom<{a: string}, {a: number}> just becomes {a: number}, which {a: number} extends. But KeysFrom<{a: string}, {b: number} becomes {b: never}, which {b: number} does not extends. Then you declare that ThingOptions extends KeysFrom<Thing, ThingOptions>. It's a recursive constraint. Let's see it:
interface ThingOptions extends KeysFrom<Thing, ThingOptions> {
foo: number;
bar: string;
}
That works.
interface BadThingOptions extends KeysFrom<Thing, BadThingOptions> { // error!
// ~~~~~~~~~~~~~~~ <-- properties of type "bap" are incompatible
foo: number;
bap: string;
}
And that fails to compile, as you presumably want.
Another thing you can do which isn't recursive is to move the check to its own line:
type KeysAreSubset<
T,
U extends { [K in keyof U]: K extends keyof T ? U[K] : never }
> = true;
Here, KeysAreSubset<T, U> evaluates to true always, but U is now constrained to KeysFrom<T, U>. If you use KeysAreSubset<ThingOptions, U> with a bad U, you'll get an error:
interface ThingOptions {
foo: number;
bar: string;
}
type ThingOptionsIsOkay = KeysAreSubset<Thing, ThingOptions>;
interface BadThingOptions {
foo: number;
bap: string;
}
type BadThingOptionsIsNotOkay = KeysAreSubset<Thing, BadThingOptions>; // error!
// Types of property 'bap' are incompatible. -----> ~~~~~~~~~~~~~~~
Both of those work, but they both require you to rewrite the name ThingOptions, and they both require you to jump through some hoops to use properly. Still, it might work for your use case. Good luck!
Link to code