Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I specify that an interface will share the keys of another?

Tags:

typescript

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?

like image 739
KOVIKO Avatar asked Dec 19 '25 21:12

KOVIKO


1 Answers

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

like image 167
jcalz Avatar answered Dec 21 '25 17:12

jcalz