I have two interfaces that I want to share the same keys, but not the same values. Imagine that the first interface (Thing) is from an external library and the second interface (ThingOptions) is in my project.
interface Thing {
foo(value: number): void;
bar(value: string): void;
someOtherKey: null;
}
interface ThingOptions {
foo: number;
bar: string;
}
Is there any way to assert that the keys of ThingOptions are a subset of the keys of Thing?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With