網站搜尋

TypeScript 介面與複合屬性名稱


我需要接受兩組命令列參數來描述兩個不同的資料庫連線(連接到 PostgreSQL 資料庫,但這對這個主題而言並不相關)。

我沒有引入可怕的代碼重複,而是製作了描述連接參數集合的通用類型的 interface。

我快完成了,但我在最後一步卡住了:編寫一個通用函數來訪問這個 interface 的屬性。

interface BasePosgresProgramOptions {
  postgresConnectionString?: string;
  postgresSrvServiceName?: string;
  postgresDb?: string;
  postgresDbFile?: string;
  postgresUser?: string;
  postgresUserFile?: string;
  postgresPassword?: string;
  postgresPasswordFile?: string;
  postgresHost?: string;
  postgresPoolMax?: number;
  postgresPort?: number;
  postgresSslCaFile?: string;
}

type ConnectionNames = "" | "Secondary";

export type PosgresProgramOptions<Suffix extends ConnectionNames> = {
  [k in keyof BasePosgresProgramOptions as `${k}${Suffix}`]: BasePosgresProgramOptions[k];
};

到目前為止一切運作良好!現在開始問題:我將函數寫成如下,但返回類型不是很令人興奮...(注意:請查看後面連結的 playground 了解詳情)

export const getPostgresOption = <Suffix extends ConnectionNames>(
  args: PosgresProgramOptions<Suffix> | undefined,
  suffix: Suffix,
  option: keyof BasePosgresProgramOptions
) => args?.[`${option}${suffix}`];

所以我嘗試通過如下更改函數來強制返回類型,但現在它甚至無法編譯了。:(

export const getPostgresOption = <Suffix extends ConnectionNames, Option extends keyof BasePosgresProgramOptions>(
  args: PosgresProgramOptions<Suffix> | undefined,
  suffix: Suffix,
  option: Option
) => args?.[`${option}${suffix}`];

我錯過了什麼?

這裡有一個遊樂場,可以輕鬆地檢查這個問題。

附註:我知道我可以透過指定來強制返回類型,但我更希望採用一種從良好程式碼中推斷類型的解決方案。

答案#1

編譯器無法對泛型類型進行太多抽象推理。如果你希望它理解 getPostgresOption() 返回的類型取決於 suffix 和 option 的類型,你需要用它可以處理的方式來表達這個操作。在以下內容中,我將使用 microsoft/TypeScript#47109 中描述的技術。


我看到的主要問題是編譯器並不真正明白你可以使用類型為 `$ {K}$ {S}` 的鍵來索引 PosgresProgramOptions<K>,其中 S extends ConnectionNames。說服它這是可能的一種方法是將 PosgresProgramOptions 改為也依賴 S。如果我們寫出類似 Pick<PosgresProgramOptions<K>, `$ {K}$ {S}`> 的東西(這是 PosgresProgramOptions 的一個超型別),那麼編譯器就肯定能夠使用 `$ {K}$ {S}` 來索引它。

像這樣:

type PosgresProgramOptions<
  S extends ConnectionNames,
  K extends keyof BasePosgresProgramOptions = keyof BasePosgresProgramOptions
> = { [P in K as `${P}${S}`]?: BasePosgresProgramOptions[P]; };

現在這個實作成功了:

const getPostgresOption = <
  S extends ConnectionNames,
  K extends keyof BasePosgresProgramOptions
>(
  args: PosgresProgramOptions<S, K> | undefined,
  suffix: S,
  option: K
) => args?.[`${option}${suffix}`]; // okay
// return type
// PosgresProgramOptions<S, K>[`${K}${S}`] | undefined

你可以看到回傳類型取決於 S 和 K。讓我們來測試它:

const sppo: SecondPosgresProgramOptions = {
  postgresConnectionStringSecondary: "cxstr",
  postgresPortSecondary: 123
}

const str = getPostgresOption(sppo, "Secondary", "postgresConnectionString");
console.log(str?.toUpperCase()); // "CXSTR"
const num = getPostgresOption(sppo, "Secondary", "postgresPort");
console.log(num?.toFixed(2)) // "123.00"

看起來不錯。str 被推斷為類型 string | undefined(我假設消除 undefined 不在本問題的範圍內;大概可以開始關注 SecondPosgresProgramOptions 的 子類型。在這裡,我們將僅依賴可選鏈來處理 undefined。)而 num 被推斷為類型 number | undefined。

程式碼的 Playground 連結