簡體   English   中英

class 方法參數類型的泛型

[英]Generic type of class method parameter type

我在嘗試正確鍵入 class 中的方法時遇到一些問題。首先,我有這個 class:

export class Plugin {
    configure(config: AppConfig) {}
    beforeLaunch(config: AppConfig) {}
    afterSetup(runtime: Runtime) {}
    afterStartup(runtime: Runtime) {}
    onUserError(error: UserError) {}
}

另一個 class 以編程方式運行它的一些方法:

export class PluginManager {
    _plugins: Plugin[];
    _pythonPlugins: any[];

    constructor() {
        this._plugins = [];
        this._pythonPlugins = [];
    }

    private setUpPlugins = (property: keyof Plugin, parameter:AppConfig | Runtime | UserError ) => {
        for (const p of this._plugins) p[property](parameter); // <- parameter is erroring out
        for (const p of this._pythonPlugins) p[property]?.(parameter);
    }

問題是 setUpPlugins 中第一個編程調用中的參數在抱怨:

Argument of type 'AppConfig | Runtime | UserError' is not assignable to parameter of type 'AppConfig & Runtime & UserError'.
  Type 'AppConfig' is not assignable to type 'AppConfig & Runtime & UserError'.
    Type 'AppConfig' is missing the following properties from type 'Runtime': config, src, interpreter, globals, and 8 more. (tsserver 2345)    

我不知道為什么它期望的類型是交集而不是聯合。 無論如何,我嘗試使用 generics 解決此問題:

    private setUpPlugins = <T extends keyof Plugin, U extends Parameters<Plugin[T]>> (property: T, parameter:U ) => {
        for (const p of this._plugins) p[property](parameter);  
        for (const p of this._pythonPlugins) p[property]?.(parameter);
    }

但現在參數抱怨一個新錯誤:

Argument of type '[config: AppConfig] | [config: AppConfig] | [runtime: Runtime] | [runtime: Runtime] | [error: UserError]' is not assignable to parameter of type 'AppConfig & Runtime & UserError'.
  Type '[config: AppConfig]' is not assignable to type 'AppConfig & Runtime & UserError'.
    Type '[config: AppConfig]' is not assignable to type 'Runtime'. (tsserver 2345) 

解決這個問題的正確方法是什么? 我試圖避免使用任何。

喜歡的版本

private setUpPlugins = (
  property: keyof Plugin, parameter: AppConfig | Runtime | UserError 
) => {
  for (const p of this._plugins) p[property](parameter); // error!
  // Argument of type 'AppConfig | Runtime | UserError' is not assignable to 
  // parameter of type 'AppConfig & Runtime & UserError'.
}

有充分理由失敗; 如果property具有聯合類型keyof Plugin並且parameter具有聯合類型AppConfig | Runtime | UserError AppConfig | Runtime | UserError AppConfig | Runtime | UserError ,沒有什么可以阻止某人使用不匹配的 arguments 調用setupPlugins() 。這些聯合是不相關的; 沒有什么能確保正確的鍵與正確的值相伴。

文檔中所述,使用編譯器知道安全的參數調用函數聯合(如p[property]的唯一方法是使用參數類型的交集 如果parameterAppConfig & Runtime & UserError類型,那么無論property什么,調用p[property](parameter)都是安全的。 但是AppConfig | Runtime | UserError AppConfig | Runtime | UserError AppConfig | Runtime | UserError不是那種類型,所以允許調用是不安全的。


理想情況下,您會限制propertyparameter正確關聯。 這可以用多種方式表示。 例如,這是您嘗試使用generics的(稍微固定的)版本:

private setUpPlugins = <K extends keyof Plugin>(
  property: K, parameter: Parameters<Plugin[K]>[0]
) => {
  for (const p of this._plugins) p[property](parameter); // error!
  // Argument of type 'AppConfig | Runtime | UserError' is not 
  // assignable to parameter of type 'AppConfig & Runtime & UserError'.
}

但是正如您所看到的,您會遇到同樣的錯誤。 這里不太可能傳入不匹配的術語,但編譯器無法遵循邏輯。 它仍然像propertyparameter一樣是不相關的聯合類型。

TypeScript 對所謂的相關聯合類型沒有很好的支持。 這是microsoft/TypeScript#30581的主題,在最長的時間里,我知道最好的做法是使用一些類型斷言並繼續。


然而,現在我們有microsoft/TypeScript#47109中描述的方法:將類型重構為“基本”映射接口,該接口清楚地表示我們關心的相關性。 然后根據分布式 object 類型執行所有操作,其中我們map 在映射接口上並立即索引到它以獲得相關聯合,如果我們需要的話。 這通常比人們認為應該做的工作要多,但它的優點是編譯器實際上可以遵循它。

根據您的示例代碼,重構可能如下所示:

interface PluginParam {
  configure: AppConfig,
  beforeLaunch: AppConfig,
  afterSetup: Runtime,
  afterStartup: Runtime,
  onUserError: UserError
}

type PluginMethods = { [K in keyof PluginParam]: (arg: PluginParam[K]) => void }

PluginMethods類型,如果你檢查它,看起來很像你的Plugins class,這是有意的。 如果Plugins是一個接口而不是 class,我們可以只使用PluginMethods 相反,我們需要保留 class。為了確保我們仍然獲得類型安全,我們可以使用一個implements子句

export class Plugin implements PluginMethods { // okay
  configure(config: AppConfig) { }
  beforeLaunch(config: AppConfig) { }
  afterSetup(runtime: Runtime) { }
  afterStartup(runtime: Runtime) { }
  onUserError(error: UserError) { }
}

現在是方法:

private setUpPlugins = <K extends keyof PluginParam>(
  property: K, parameter: PluginParam[K]
) => {
  const _plugins: PluginMethods[] = this._plugins;
  for (const p of _plugins) p[property](parameter); // okay
}

我們將一個新的_plugins變量注釋PluginMethods[]類型並將this._plugins分配給它。 這似乎毫無意義,但它是為了讓編譯器理解this._plugins可以被視為PluginMethodsPluginParam上的映射類型。 然后,當我們調用p[property](parameter)時,一切正常。 p[property]被視為(arg: PluginParam[K]) => void類型,並且parameter被視為PluginParam[K]類型。 所以 function 不是 function 類型的聯合,它是一個通用的 function,其參數與parameter的類型相同。 因此它編譯沒有錯誤。

您會注意到,如果您更改其中任何一項(例如,取消const _plugins的注釋),錯誤將重新出現; 編譯器將丟失線程,您將回到開始的地方。 只有將所有內容顯式表示為基本映射接口的通用映射版本,我們才能讓編譯器盡可能安全地驗證我們正在做的事情。

游樂場代碼鏈接

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM