網站搜尋

typescript - 在類別實例中追蹤狀態


我想創建一個具有一些內部狀態(可能是加載中、錯誤或成功)的類。我還想在這個類上擁有一些可以檢查該類狀態的方法。

理想的 API:

function f(x: LoadingError<number>) {
  if (x.isLoading()) {
  } else if (x.isError()) {
  } else {
    (x.data); // TypeScript knows x.data is of type `number`
  }
}

我主要在掙扎的是如何創建 isLoading 和 isError 方法,以便 TypeScript 可以理解它們。

我試著在實際的類結構上撰寫一些使用者自訂型別守衛("" this is { ... }"):

class Foo {
  public value: string | null;
  public hasValue(): this is { value: string } {
    return this.value !== null;
  }
}

const foo = new Foo();
if (foo.hasValue()) {
  // OK
  foo.value.toString();
} else {
  (foo.value); // TypeScript does NOT understand that this can only be null
}

然而,那行不通,因為 TypeScript 在 else 子句中會「忘記」類別實例的狀態。

我的一項硬性要求是為此使用類別,因為我不想要有isLoading(instance)或isError(instance)方法,而是希望有instance.isLoading()和instance.isError()。

答案#1

我想創建一個有一些內部狀態(可能是加載中、錯誤或成功)的類別

建立你可能的狀態類型

type State<T> = ErrorState | SuccessState<T> | LoadingState;

type ErrorState = { status: "error"; error: unknown };
type SuccessState<T> = { status: "success"; data: T };
type LoadingState = { status: "loading" };

我也想在這個類別中擁有一些方法,可以檢查這個類別的狀態。

創建包含狀態的 Foo 類

我在這裡假設,你想要調用某種公共類型守衛方法 isSuccess、isLoading、isError,它可以檢查你的類實例狀態,並且可以在 if/else 的真分支中縮小狀態類型。你可以通過創建返回多態 this 類型謂詞的類型守衛來實現,該謂詞包含你縮小的狀態。

// T is the possible data type of success state
class Foo<T = unknown> {
  constructor(public readonly currentState: State<T>) {}

  isLoading(): this is { readonly currentState: LoadingState } {
    return this.currentState.status === "loading";
  }

  isSuccess(): this is { readonly currentState: SuccessState<T> } {
    return this.currentState.status === "success";
  }

  isError(): this is { readonly currentState: ErrorState } {
    return this.currentState.status === "error";
  }
}

讓我們來測試它:

const instance = new Foo({ status: "success", data: 42 });
if (instance.isSuccess()) {
  // works, (property) data: number
  instance.currentState.data; 
}

遊樂場

限制

重點來了:你只能在已將類別成員 currentState 聲明為 public 修飾符時才能這麼做(TypeScript 限制)!如果你將它聲明為 private,就不能為此目的使用這種類型保護。另一種方法是返回可選的狀態:

class Foo<T = unknown> {
... 
  getSuccess(): SuccessState<T> | null {
    return this.currentState.status === "success" ? this.currentState : null;
  }
...
}

// test it 
const instance = new Foo({ status: "success", data: 42 });
const state = instance.getSuccess()
if (state !== null) {
  // works, (property) data: number
  state.data
}

遊樂場

關於你在 foo.hasValue() 上的分支錯誤的附註:

const foo = new Foo();
if (foo.hasValue()) {
  // OK
  foo.value.toString();
} else {
  (foo.value); // TypeScript does NOT understand that this can only be null
}

TypeScript 在這裡不會將 foo.value 推斷為 null,因為 foo.hasValue() 是一個自定義類型保護(Type Guard),它只是將類型縮小到 { value: string },條件為 true。如果條件為 false,則會再次假設 value 的預設類型為 string | null。自定義類型保護會取消 TypeScript 的正常分支邏輯。你可以簡單地省略它來改變這個行為:

if (foo.value !== null) {
  // OK
  foo.value.toString();
} else {
  (foo.value); // (property) Foo.value: null
}

遊樂場

如果你從類別實例內檢查你的狀態

class Foo<T = unknown> {
  ...
  // Define private custom type guard. We cannot use a polymorphic
  // this type on private attribute, so we pass in the state directly.
  private _isSuccess(state: State<T>): state is SuccessState<T> {
    return state.status === "success";
  }

  public doSomething() {
    // use type guard
    if (this._isSuccess(this.currentState)) {
      //...
    }

    // or inline it directly
    if (this.currentState.status === "success") {
      this.currentState.data;
      //...
    }
  }
}

遊樂場

答案#2

正如我們在其他答案中看到的,對此有很多解決方案。

從閱讀這個問題可以看出你的需求是:

  1. 使用一個類來表示狀態的聯合
  2. 使用類別成員來縮小到一個狀態
  3. 使收窄在兩個分支上都正確運作。

最大問題是 3。縮小會在真分支上通過與類型保護所斷言的類型相交來工作。else 分支則通過從變數類型中排除被斷言的類型來工作。如果類型是一個聯合類型且編譯器可以排除聯合的所有匹配成分,這種方法效果很好,但在這裡我們有一個類,因此沒有任何成分可以排除,我們只剩下原始的 Foo 類型。

在我看來,最簡單的解決方案是將類的實例類型與實際類解耦。我們可以將構造函數類型設置為返回與適當狀態聯合的聯合類型。這將允許排除機制在 else 分支上按預期工作:

class _Foo {
  public value: string | null =  null;
  public hasValue(): this is { value: string } {
    return this.value !== null;
  }
}

const Foo : new () => _Foo & 
    ({ value: string } | { value: null }) 
    = _Foo as any;


const foo = new Foo();
if (foo.hasValue()) {
  // OK
  foo.value.toString();
} else {
  (foo.value); // TypeScript does NOT understand that this can only be null
}

播放

我們可以混合幾種狀態:

class _Foo {
    public value: string | null = null;
    public error: string | null = null;
    public progress: string | null = null
    public isError(): this is { error: string } { // we just need to specify enough to select the desired state, only one state below has error: string
        return this.error !== null;
    }
    public isLoading(): this is { progress: string } { // we just need to specify enough to select the desired state, only one state below has progress: string
        return this.value === null && this.progress !== null;
    }
}

const Foo: new () => _Foo & ( 
    | { value: string, error: null, progress: null }  // not loading anymore
    | { value: null, error: null, progress: string }  // loading
    | { value: null, error: string, progress: null})
    = _Foo as any;


const foo = new Foo();
if (foo.isError()) {
    // we got an error
    foo.progress // null
    foo.value // null
    foo.error.big() // string
} else if (foo.isLoading()) {
    // Still loading
    foo.progress // string
    foo.value // null
    foo.error // null
} else {
    // no error, not loading we have a value
    foo.value.big() // string
    foo.error // null
    foo.progress // null
}

播放

唯一的限制是,在類別內部,守衛將不起作用。

供您參考,如果您有排除所有狀態的類型保護,您甚至可以使用 assertNever 技巧來確保所有狀態都已處理:play