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