typescript - Kan jeg spesifisere at et interface skal dele nøklene til et annet?


Jeg har to grensesnitt som jeg vil skal dele de samme nøklene, men ikke de samme verdiene. Tenk deg at det første grensesnittet (Thing) er fra et eksternt bibliotek, og det andre grensesnittet (ThingOptions) er i mitt prosjekt.

interface Thing {
  foo(value: number): void;
  bar(value: string): void;
  someOtherKey: null;
}

interface ThingOptions {
  foo: number;
  bar: string;
}

Finnes det en måte å hevde at nøklene til ThingOptions er et delsett av nøklene til Thing?

Svar #1

Det finnes noen måter å få litt kompileringstidssjekking på dette, men jeg tror alle krever litt gjentakelse av navnet ThingOptions.

En måte er å lage en KeysFrom<T, U> som tar en type T og en kandidat-type U, som bare er kompatibel med U hvis nøklene til U er et delsett av nøklene til T (som betyr at T kan ha nøkler som U ikke har, men U kan ikke ha nøkler som T ikke har):

type KeysFrom<T, U> = { [K in keyof U]: K extends keyof T ? U[K] : never }

Så KeysFrom<{a: string}, {a: number}> blir bare {a: number}, som {a: number} utvider. Men KeysFrom<{a: string}, {b: number} blir {b: never}, som {b: number} ikke utvider. Så erklærer du at ThingOptions utvider KeysFrom<Thing, ThingOptions>. Det er en rekursiv begrensning. La oss se på det:

interface ThingOptions extends KeysFrom<Thing, ThingOptions> {
    foo: number;
    bar: string;
}

Det fungerer.

interface BadThingOptions extends KeysFrom<Thing, BadThingOptions> { // error!
//        ~~~~~~~~~~~~~~~ <-- properties of type "bap" are incompatible
    foo: number;
    bap: string;
}

Og det feiler å kompilere, som du antagelig ønsker.

En annen ting du kan gjøre som ikke er rekursiv, er å flytte sjekken til sin egen linje:

type KeysAreSubset<
    T,
    U extends { [K in keyof U]: K extends keyof T ? U[K] : never }
> = true;

Her evalueres KeysAreSubset<T, U> alltid til true, men U er nå begrenset til KeysFrom<T, U>. Hvis du bruker KeysAreSubset<ThingOptions, U> med en dårlig U, vil du få en feil:

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. -----> ~~~~~~~~~~~~~~~

Begge disse fungerer, men de krever begge at du skriver om navnet ThingOptions, og de krever begge at du går gjennom noen hindringer for å bruke det riktig. Likevel kan det fungere for ditt brukstilfelle. Lykke til!

Lenke til kode

Svar #2

Det enkle svaret er: NEI, det kan du ikke. De to grensesnittene er forskjellige, og egenskapene deres har ulike typer.

Så selv om du ikke kan bruke TypeScript til å gjøre noen sikkerhetstiltak, kan du gjøre dette:

  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...