繁体   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