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
正如我們在其他答案中看到的,對此有很多解決方案。
從閱讀這個問題可以看出你的需求是:
- 使用一個類來表示狀態的聯合
- 使用類別成員來縮小到一個狀態
- 使收窄在兩個分支上都正確運作。
最大問題是 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
