網站搜尋

typescript - 如何在類別欄位中對 this 進行映射?


給定一個映射到類別欄位的欄位,忽略自身並忽略值為字串的鍵:

class Foo {

  x = 1;
  y = 'y';

  $ = {} as { [K in keyof this as K extends '$' ? never : this[K] extends string ? never : K]: number };

  bar() {
    this.$.x
  }

}

這不應該出錯,並且應該提供自動完成。但 this.$.x 說:

Property 'x' does not exist on type '{ [K in keyof this as K extends "$" ? never : this[K] extends string ? never : K]: number; }'.ts(2339)

事實上,在這個映射類型中 沒有 任何欄位。為什麼?我們要如何修正它?

答案#1

在 microsoft/TypeScript#4910 中實作的多型 this 類型是 隱含 泛型的:“多型 this 類型是通過為每個類和介面提供一個隱含的類型參數來實作的,而該參數被限制為包含它的類型本身”。所以它在類的內部表現得像一個泛型類型參數。因此 TypeScript 並不真正知道如何對它執行任意的類型操作。當你對它執行這類操作時,要麼操作是 延遲 的,在這種情況下 TypeScript 不知道它會是什麼,直到在類外部指定 this,要麼操作是 過早解析 的,這意味著它被視為其約束類型,這可能適合,也可能不適合。

在你的範例中,看起來你的條件型別 this[K] extends string ? ⋯ : ⋯ 是延遲的,而這使得你的映射型別對 TypeScript 完全不可穿透。它甚至不會試圖去弄清楚屬性可能存在或不存在的原因。所以它會對每個鍵都提出警告。

這是 TypeScript 在使用泛型(因此也包括多態的 this)時的一般設計限制,尤其是它們與條件類型的互動。參見 microsoft/TypeScript#57417 以了解類似問題。你要麼放棄,要麼想辦法規避它。


最簡單的解決方法就是選擇自行積極解析泛型,方法是將 this 替換為其約束 Foo:

type MyType<T> = 
  { [K in keyof T as K extends '$' ? never : T[K] extends string ? never : K]: number }

class Foo {
    x = 1;
    y = 'y';
    $ = {} as MyType<Foo> // instead of MyType<this>
    bar() {
        this.$.x // okay
    }    
}

當然,那個過早的規範意味著子類別無法在 $ 上獲得額外的魔法屬性。根據使用情況,你可以通過將 this 版本與非 this 版本交集,使類型更複雜:

class Foo {
    x = 1;
    y = 'y';
    $ = {} as MyType<this> & MyType<Foo> // intersect
    bar() {
        this.$.x
    }    
}    

class Bar extends Foo {
    z = 200;
}
const bar = new Bar();
bar.$.z.toFixed(); // okay, subclass instance sees new property

不過,這在子類的內部仍然無法運作。你需要在每個子類中手動縮小 $(例如使用 declare 關鍵字)才能讓它運作:

class Baz extends Bar {
    w = new Date();
    declare $: MyType<this> & MyType<Baz>
    method() {
        this.$.w.toFixed();
    }
}

不,這些都不完美。

程式碼的 Playground 連結