typescript - Kan jeg skrive en typevakt som bekrefter flere invariant?


Kan jeg skrive en typevakt som fastslår noe om ett eller flere underobjekter av et argument? I pseudo-kode kan det se slik ut:

class C {
    a: number?;
    b: string?;

    function assertInitialized() : (this.a is number) and (this.b is string) {
        return this.a !== null && this.b !== null;
    }
}


Bakgrunn: Jeg pleier vanligvis å bruke en enkelt funksjon for å sjekke invarianter i klassene mine; for eksempel, anta at jeg har en klasse med noen nullable felter, som blir initialisert asynkront.

class DbClient {
    dbUrl: string;
    dbName: string;
    dbConnection: DBConnection?;
    …
}

Denne klassen går gjennom en kompleks asynkron initialiseringsprosess, hvoretter dbConnection blir ikke-null. Brukere av klassen må ikke kalle visse metoder før dbConnection er initialisert, så jeg har en funksjon assertReady:

assertReady() {
    if (this.dbConnection === null) {
        throw "Connection not yet established!";
    }
}

Denne funksjonen assertReady kalles i begynnelsen av hver funksjon som krever at DbClient er fullstendig initialisert, men jeg må fortsatt skrive ikke-null-påstander:

fetchRecord(k: string) {
    assertReady();
    return this.dbConnection!.makeRequest(/* some request based on k */);
}

Kan jeg gi assertReady en signatur som gjør ! unødvendig? Jeg vil ikke sende this.dbConnection til assertReady, fordi den funksjonen vanligvis er mer kompleks.

Det eneste trikset jeg kjenner til er å lage et grensesnitt med de samme feltene som den nåværende klassen, men med ikke-nullbare typer (uten ?). Deretter kan jeg lage en typebeskytter som sier at this is InitializedDbClient. Dessverre krever det å duplisere en stor del av klassedefinisjonen. Finnes det en bedre måte?

Svar #1

Ja, du kan og du hadde det nesten helt riktig i pseudokode

interface A {
  a?: number;
  b?: string;

  hasAandB(): this is {a: number} & {b: string};
}

Legg merke til hvordan pseudokoden din sin and ble til en &. Veldig nært, faktisk.

Selvfølgelig er det ikke nødvendig å bruke den operatøren, typen intersection operatør, i dette tilfellet fordi vi kan forenkle det til

hasAandB(): this is {a: number, b: string};

Men forestill deg at vi legger til en tredje egenskap, si c, som ikke påvirkes av typevakten, og likevel ikke ønsker å miste dens bidrag til resultattypen uansett.

Intuisjonen din om komponerbare typebeskyttere bringer oss tilbake til utgangspunktet

hasAandB(): this is this & {a: number, b: string};

Det finnes alle slags virkelig interessante og svært nyttige ting du kan gjøre med disse mønstrene.

For eksempel kan du sende mange eigenskapnøkler generisk basert på typen til et objekt, og avhengig av de faktiske nøklene du sender, kan resultatet være typebeskyttet til skjæringen av en bærer av hver eigenskap.

function hasProperties<T, K1 extends keyof T, K2 extends keyof T>(
  x: Partial<T>,
  key1: K1,
  key2: K2
): x is Partial<T> & {[P in K1 | K2]: T[P]} {
  return key1 in x && key2 in x;
}


interface I {
  a: string;
  b: number;
  c: boolean;
}

declare let x: Partial<I>;

if (hasProperties(x, 'a', 'b')) {...}

Og virkelig, dette skraper bare overflaten av hva som er mulig.

En annen virkelig interessant anvendelse av dette er å definere vilkårlig generiske og typesikre byggere og sammensettbare fabrikker.

Relaterte artikler