Villkorlig typ baserad på värdet av en variabel i en klass (TypeScript)


Jag skapar en generell klass som heter Loadable<T>, med två fält. Det ena heter state och innehåller det nuvarande tillståndet för den laddningsbara resursen. Det andra heter data och innehåller värdet av den laddningsbara resursen.

state definieras som "loading" | "loaded" | "error", och jag skulle vilja att typen av data ändras baserat på det aktuella värdet av state.

Till exempel, om state är "loading", då bör data vara null. Jag kan inte lista ut syntaxen för att göra detta.

Jag har skapat en typ som heter LoadableState<T, U> som definieras som:

    type LoadableState<T, U> =
        T extends "loaded" ? U :
        T extends "loading" ? null :
        T extends "error" ? string :
        never;

Där T anges måste vara typen "loaded" | "loading" | "error".

Den här delen fungerar, men att försöka definiera data från detta fungerar inte.

export class Loadable<T> {
    public state: "loaded" | "loading" | "error" = "loading";

    public data: LoadableState<state, T>;
    //                         ^^^^^ Cannot find name 'state`
}

TypeScript kastar följande fel: Kan inte hitta namnet 'state'

Andra saker jag har provat:

public data: LoadableState<this.state, T>;
// Duplicate identified 'state'

public data: LoadableState<typeof this.state, T>;
// Cannot find name 'this'

public data: LoadableState<typeof state, T>;
// Cannot find name 'state'

public data: state extends "loaded" ? T :
    state extends "loading" ? null :
    state extends "error" ? string :
    never;
// Cannot find name 'state'

Jag är inte säker på om detta faktiskt är möjligt. Om detta inte är det, finns det en annan lösning på detta problem?

Svar #1

Jag tror inte att det finns ett sätt (eller åtminstone inte ett rent sätt) att låta typen för en egenskap i en klass bero på det aktuella värdet av en annan egenskap.

Du har det bättre på att kombinera båda dessa egenskaper till ett enda objekt:

type LoadState<T> = {
    state: "loading",
} | {
    state: "loaded",
    data: T
} | {
    state: "error",
    data: string; // or maybe 'message'
}

export class Loadable<T> {
    state: LoadState<T> = { state: "loading" }
}

Även om klassomslaget egentligen inte är särskilt användbart här, kan du överväga att inte använda klassen alls.

När du använder denna typ kommer Typescript att se till att du kontrollerar state innan du försöker komma åt data:

function handleLoadState<T>(loadState: LoadState<T>) {
   loadState.data // ERROR, can't access data, since it might not exist
   if(loadState.state === "loaded")
       loadState.data // okay
   }
}

Detta mönster kallas en Diskriminerad Union och används ganska ofta i Typescript.

Relaterade artiklar