Once you accept the sad truth that you'll need to maintain some runtime artifact related to V, since V itself will be erased, and assuming you can't actually use the runtime artifact to define V, the best you can do is have the compiler yell at you if your runtime artifact and V are out of sync. Here's one way to do it:
interface V {
a: number;
b: string;
}
const keysOfV = ["a", "b"] as const;
type MutuallyAssignable<T extends U, U extends V, V = T> = void;
// the following line will give an error if keysOfV is wrong
type KeysOfVIsGood = MutuallyAssignable<keyof V, typeof keysOfV[number]>; // okay
Note the as const in the definition of keysOfV. That's a const assertion and it (or something like it) is needed to have the compiler keep track of the literal string elements of keysOfV instead of inferring the correct-but-too-wide type string[].
Then, MutuallyAssignable<T, U> is a type that evaluates to void, but we don't really care about what it evaluates to. What we care about is that T is constrained to U, and U is constrained to T (via a default parameter to sidestep a circular constraint violation). When you use MutuallyAssignable<X, Y> on some types X and Y, you will get a compiler error if the compiler does not recognize that X and Y are mutually assignable.
Then you can go on to define and use your hasInvalidProperties() function however you want, using keysOfV. Perhaps like this:
function hasInvalidProperties(x: object): x is { [K in keyof V]: Record<K, any> }[keyof V] {
return Object.keys(x).some(k => hasInvalidProperties.badKeySet.has(k));
}
hasInvalidProperties.badKeySet = new Set(keysOfV) as Set<string>;
/// test
function getDataFromRequest(): object {
return Math.random() < 0.5 ? { c: "okay" } : { a: "bad" };
}
const x = getDataFromRequest();
if (hasInvalidProperties(x)) {
console.log("not okay");
throw new Error();
}
console.log("okay");
The main event though is what happens when keysOfV is wrong. Here's what happens when it's missing an entry:
const keysOfV = ["a"] as const;
type MutuallyAssignable<T extends U, U extends V, V = T> = void;
// the following line will give an error if keysOfV is wrong
type KeysOfVIsGood = MutuallyAssignable<keyof V, typeof keysOfV[number]>; // error!
// "b" is not assignable to "a" ------> ~~~~~~~
And here's what happens when it has an extra entry:
const keysOfV = ["a", "b", "c"] as const;
type MutuallyAssignable<T extends U, U extends V, V = T> = void;
// the following line will give an error if keysOfV is wrong
type KeysOfVIsGood = MutuallyAssignable<keyof V, typeof keysOfV[number]>; // error!
// "c" is not assignable to "a" | "b" ---------> ~~~~~~~~~~~~~~~~~~~~~~
Hopefully those error messages and locations are descriptive enough for you to understand how to fix it when V changes.
Okay, hope that helps; good luck!
Link to code
Vis completely erased by runtime. SohasInvalidProperties()will not be able to consult it. You need to do something like keep an array of invalid strings.