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

Viewed 308

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?

2 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

The simple answer is: NO, you can't. Those 2 interfaces are different, their properties are of different types.

So while you can't use TypeScript to do some safe-guard, you could do this:

  let a = {
    foo: v => "my string return value",
    bar: v => "2"
  } as Thing;

  let b = {
    foo: 1,
    bar: "2"
  } as ThingOptions;

  let propertiesOfA = Object.keys(a);
  let propertiesOfB = Object.keys(b);

  // and now you just have to compare if every item in the 1st array is inside the 2nd one... so possibly creating some function of comparing 2 objects this way is the solution you need...

Related