簡體   English   中英

TypeScript:我可以使用類型泛型來返回不同的值並避免兩次定義相同的函數嗎?

[英]TypeScript: Can I use type generics to return a different value and avoid defining the same function twice?

我有一個帶有返回用戶位置的函數的 React 項目,它可以返回位置對象GeoLocation或錯誤對象(如果發生這種情況) GeoError

export function getPosition(): GeoLocation | GeoError {
  // ...
}

問題是,這個函數的大多數用法只關心是否有位置對象,為了避免檢查返回對象的類型,我添加了一個默認參數來指示調用者是否想要錯誤:

export function getPosition(withError: boolean = false): GeoLocation | GeoError | undefined {
  let position: GeoLocation = ...;
  let positionError: GeoError = ...;

  if (!position) {
    if (withError)
      return positionError;
    else
      return undefined;
  }
  return position;
}

然后不需要GeoError的調用者可以強制轉換並避免檢查返回值的類型:

let userPosition = getPosition() as GeoLocation | undefined;
if (!userPosition) {
  alert("No position!")
  return;
}
// ... do something with position

但我想避免強制轉換,所以我發現有兩個功能很好:

function getPosition(): GeoLocation | undefined;
function getPositionOrError(): GeoLocation | GeoError;

但我不想用不同的返回值實現兩次編寫相同的位置檢索邏輯,所以我做了這樣的事情:

function _getPosition(withError: boolean = false): GeoLocation | GeoError | undefined {
  let position: GeoLocation = ...;
  let positionError: GeoError = ...;

  if (!position) {
    if (withError)
      return positionError;
    else
      return undefined;
  }
  return position;
}
export function getPositionOrError(): GeoLocation | GeoError {
  return _getPosition(true) as GeoLocation | GeoError;
}
export function getPosition(): GeoLocation | undefined {
  return _getPosition(false) as GeoLocation | undefined;
}

哪個有效,但在我看來,轉換看起來有點丑陋,所以我想知道是否可以使用類型泛型讓編譯器幫助並推斷類型:

function _getPosition<WError=false>(): GeoLocation | WError ? GeoError : undefined {
  let position: GeoLocation = ...;
  let positionError: GeoError = ...;

  if (!position) {
    if (WError)
      return positionError;
    else
      return undefined;
  }
  return position;
}

但不幸的是,這不起作用,而且我不知道我可以查找哪些搜索詞來正確地做到這一點,或者它是否可能。

謝謝!

使用重載怎么樣?

function getPosition(): GeoLocation | undefined;
function getPosition(withError: true): GeoLocation | GeoError;
function getPosition(withError: boolean = false): GeoLocation | GeoError | undefined {
  let position: GeoLocation = ...;
  let positionError: GeoError = ...;

  if (!position) {
    if (withError)
      return positionError;
    else
      return undefined;
  }
  return position;
}

正如@Andreas 的回答所述,您可以使用重載 這很簡單:

function getPosition(withError?: false): GeoLocation | undefined;
function getPosition(withError: true): GeoLocation | GeoError;
function getPosition(withError: boolean = false): GeoLocation | GeoError | undefined {
  let position: GeoLocation = null!;
  let positionError: GeoError = null!;
  if (!position) {
    if (!withError) // oops!
      return positionError;
    else
      return undefined;
  }
  return position;
}

const y = getPosition(true); // GeoLocation | GeoError;
const n = getPosition(false); // GeoLocation | undefined;
const n2 = getPosition(); // GeoLocation | undefined;

主要缺點是編譯器並沒有很好地檢查實現。 例如,您可以在您的實現中翻轉一個布爾值,而編譯器不會捕獲它。 事實上,我在上面這樣做了 ( // oops! ) 並且沒有編譯器錯誤。 不過,這仍然是一種合理的前進方式。


如果您想按照問題中描述的方式使用泛型,那么您希望將函數的返回類型設為條件類型 所以調用簽名可以是:

declare function getPosition<B extends boolean = false>(
  withError: B = false as B
): GeoLocation | (B extends true ? GeoError : undefined);

因此,如果withErrortrue ,則B類型參數將為true ,如果withErrorfalse或被省略,則為false (因為默認類型參數false默認函數參數也是如此)。 返回類型是GeoLocation與條件類型B extends true? GeoError: undefined聯合 B extends true? GeoError: undefined

請注意,這還允許您為withError傳遞一個boolean ,返回類型將為GeoLocation | GeoError | undefined GeoLocation | GeoError | undefined GeoLocation | GeoError | undefined 像這樣:

const y = getPosition(true); // GeoLocation | GeoError;
const n = getPosition(false); // GeoLocation | undefined;
const n2 = getPosition(); // GeoLocation | undefined;
const yn = getPosition(Math.random() < 0.5) // GeoLocation | GeoError | undefined

不幸的是,實現具有條件返回類型的泛型函數很煩人。 編譯器仍然無法驗證您在實現中所做的事情是否安全,這一次它會抱怨您返回的所有內容,而不是接受您返回的所有內容。 也就是說,你會得到錯誤的警告而不是無聲的失敗:

export function getPosition<B extends boolean = false>(
  withError: B = false as B
): GeoLocation | (B extends true ? GeoError : undefined) {
  let position: GeoLocation = null!;
  let positionError: GeoError = null!;

  if (!position) {
    if (withError)
      return positionError as (B extends true ? GeoError : undefined);
    else
      return undefined as (B extends true ? GeoError : undefined);
  }
  return position;
}


export function getPosition<B extends boolean = false>(
  withError: B = false as B
): GeoLocation | (B extends true ? GeoError : undefined) {
  let position: GeoLocation = null!;
  let positionError: GeoError = null!;

  if (!position) {
    if (withError)
      return positionError; // error!
    else
      return undefined; // error!
  }
  return position;
}

這是 TypeScript 的限制; 請參閱microsoft/TypeScript#33912上的功能請求,尋求更好的東西。 現在,您需要類型斷言之類的東西來抑制錯誤(您稱之為“轉換”):

export function getPosition<B extends boolean = false>(
  withError: B = false as B
):
  GeoLocation | (B extends true ? GeoError : undefined) {
  let position: GeoLocation = null!;
  let positionError: GeoError = null!;

  if (!position) {
    if (withError)
      return positionError as (B extends true ? GeoError : undefined);
    else
      return undefined as (B extends true ? GeoError : undefined);
  }
  return position;
}

這有效但讓我們回到靜默失敗而不是錯誤警告......你可以將if (withError)更改為if (!withError)並且不會有錯誤。

因此,是否需要重載或條件泛型取決於您。 沒有一個是完美的。


現在,可以從實現中獲得某種類型的安全性,但坦率地說它很丑陋和奇怪。 編譯器非常擅長驗證對通用索引訪問類型的可分配性,只要您實際索引到具有通用鍵的對象即可。 你的withError輸入不像一個鍵,它是一個boolean (或undefined )。 但是我們可以使用模板文字truefalseundefined轉換為字符串文字"true""false""undefined" ,並將它們用作鍵。 並且編譯器可以使用模板字面量類型來遵循這個邏輯。

這里是:

function getPosition<T extends [withError?: false] | [withError: true]>(
  ...args: T
) {
  let position: GeoLocation = null!;
  let positionError: GeoError = null!;
  if (!position) {
    const withError: T[0] = args[0];
    return {
      true: positionError,
      false: undefined,
      undefined: undefined,
    }[`${withError}` as const];
  }
  return position;
}

我在args rest 參數的rest 元組類型T中使函數泛型化,這樣編譯器就會明白,即使withError輸入也可用於確定泛型類型參數。 withError的類型是T[0]

然后我們索引一個對象,並將其序列化版本作為鍵。 這會產生以下瘋狂的調用簽名類型:

/* function getPosition<
  T extends [withError?: false | undefined] | [withError: true]
>(...args: T): GeoLocation | {
  true: GeoError;
  false: undefined;
  undefined: undefined;
}[`${T[0]}`] */

但它有效:

const y = getPosition(true); // GeoLocation | GeoError;
const n = getPosition(false); // GeoLocation | undefined;
const n2 = getPosition(); // GeoLocation | undefined;
const yn = getPosition(Math.random() < 0.5) // GeoLocation | GeoError | undefined

而這一次,如果您更改實現,輸出類型將相應更改:

    return {
      true: undefined,
      false: positionError,
      undefined: positionError,
    }[`${withError}` as const];

const y = getPosition(true); // GeoLocation | undefined;
const n = getPosition(false); // GeoLocation | GeoError;

所以編譯器真的在為你驗證類型。 萬歲!

但代價是什么? 我不想在任何生產環境中看到這樣的代碼; 重載或什至帶有類型斷言的通用條件更合理,即使它們在實現中不是類型安全的。 類型安全並不總是比慣用的編碼風格更重要。

游樂場代碼鏈接

暫無
暫無

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

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