简体   繁体   English

TypeScript:我可以使用类型泛型来返回不同的值并避免两次定义相同的函数吗?

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

I have a React project with a function that returns the user's position and it can either return a location object, GeoLocation , or an error object if such occurs, GeoError :我有一个带有返回用户位置的函数的 React 项目,它可以返回位置对象GeoLocation或错误对象(如果发生这种情况) GeoError

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

The thing is, most usages of this function only care about whether there is a location object or not, and to avoid checking the type of the returned object I added a default parameter that indicates whether the caller wants the error:问题是,这个函数的大多数用法只关心是否有位置对象,为了避免检查返回对象的类型,我添加了一个默认参数来指示调用者是否想要错误:

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;
}

Then the callers that don't want the GeoError can just cast and avoid checking the type of the return value:然后不需要GeoError的调用者可以强制转换并避免检查返回值的类型:

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

But I want to avoid casting, so I figured out having two functions is nice:但我想避免强制转换,所以我发现有两个功能很好:

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

but I don't want to write the same location retrieval logic twice with different implementations for the return value, so I did something like this:但我不想用不同的返回值实现两次编写相同的位置检索逻辑,所以我做了这样的事情:

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;
}

Which works but seems kinda ugly with the cast in my opinion, so I wondered if it's possible to use type generics to let the compiler help and infer the type:哪个有效,但在我看来,转换看起来有点丑陋,所以我想知道是否可以使用类型泛型让编译器帮助并推断类型:

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;
}

but this unfortunately doesn't work, and I don't know what search terms I can lookup to do it correctly or whether it's possible at all.但不幸的是,这不起作用,而且我不知道我可以查找哪些搜索词来正确地做到这一点,或者它是否可能。

Thanks!谢谢!

How about using overloading?使用重载怎么样?

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;
}

As @Andreas's answer states, you could use overloads ;正如@Andreas 的回答所述,您可以使用重载 this is straightforward to do:这很简单:

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;

The main drawback is that the compiler doesn't really check the implementation very well;主要缺点是编译器并没有很好地检查实现。 you could, for example, flip a boolean in your implementation, and the compiler wouldn't catch it.例如,您可以在您的实现中翻转一个布尔值,而编译器不会捕获它。 In fact, I did that above ( // oops! ) and there's no compiler error.事实上,我在上面这样做了 ( // oops! ) 并且没有编译器错误。 This is still a reasonable way forward, though.不过,这仍然是一种合理的前进方式。


If you want to use generics the way you're describing in your question, then you're looking to make the return type of the function a conditional type .如果您想按照问题中描述的方式使用泛型,那么您希望将函数的返回类型设为条件类型 So the call signature could be:所以调用签名可以是:

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

So the B type parameter would be true if withError is true , and false if withError is false , or if it is omitted (because the default type parameter is false , and so is the default function parameter ).因此,如果withErrortrue ,则B类型参数将为true ,如果withErrorfalse或被省略,则为false (因为默认类型参数false默认函数参数也是如此)。 And the return type is the union of GeoLocation with the conditional type B extends true? GeoError: undefined返回类型是GeoLocation与条件类型B extends true? GeoError: undefined联合 B extends true? GeoError: undefined . B extends true? GeoError: undefined

Note that this also allows you to pass in a boolean for withError , and the return type will be GeoLocation | GeoError | undefined请注意,这还允许您为withError传递一个boolean ,返回类型将为GeoLocation | GeoError | undefined GeoLocation | GeoError | undefined GeoLocation | GeoError | undefined . GeoLocation | GeoError | undefined Like this:像这样:

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

Unfortunately implementing generic functions with conditional return types is annoying.不幸的是,实现具有条件返回类型的泛型函数很烦人。 The compiler still cannot verify that what you are doing in the implementation is safe, and this time it complains about everything you return instead of accepting everything you return.编译器仍然无法验证您在实现中所做的事情是否安全,这一次它会抱怨您返回的所有内容,而不是接受您返回的所有内容。 That is, you'll get false warnings instead of silent failures:也就是说,你会得到错误的警告而不是无声的失败:

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;
}

This is a limitation of TypeScript;这是 TypeScript 的限制; see the feature request at microsoft/TypeScript#33912 asking for something better.请参阅microsoft/TypeScript#33912上的功能请求,寻求更好的东西。 For now, you need something like type assertions to suppress the error (you call this "casting"):现在,您需要类型断言之类的东西来抑制错误(您称之为“转换”):

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;
}

which works but gets us back to silent failures instead of false warnings... you could change if (withError) to if (!withError) and there'd be no error.这有效但让我们回到静默失败而不是错误警告......你可以将if (withError)更改为if (!withError)并且不会有错误。

So it's kind of up to you whether you want overloads or conditional-generics.因此,是否需要重载或条件泛型取决于您。 Neither one is perfect.没有一个是完美的。


Now, it is possible to get some type safety from the implementation, but it's frankly ugly and bizarre.现在,可以从实现中获得某种类型的安全性,但坦率地说它很丑陋和奇怪。 The compiler is pretty good at verifying assignability to generic indexed access types as long as you are actually indexing into an object with a generic key.编译器非常擅长验证对通用索引访问类型的可分配性,只要您实际索引到具有通用键的对象即可。 Your withError input isn't like a key though, it's a boolean (or undefined ).你的withError输入不像一个键,它是一个boolean (或undefined )。 But we can use template literals to turn true , false , or undefined , into the string literals "true" , "false" , or "undefined" , and use those as keys.但是我们可以使用模板文字truefalseundefined转换为字符串文字"true""false""undefined" ,并将它们用作键。 And the compiler can use template literal types to follow this logic.并且编译器可以使用模板字面量类型来遵循这个逻辑。

Here it is:这里是:

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;
}

I'm making the function generic in the rest tuple type T of the args rest parameter, so that the compiler will understand that even leaving out the withError input can be used to determine the generic type argument.我在args rest 参数的rest 元组类型T中使函数泛型化,这样编译器就会明白,即使withError输入也可用于确定泛型类型参数。 The type of withError is T[0] . withError的类型是T[0]

And then we index into an object with the serialized version of that as keys.然后我们索引一个对象,并将其序列化版本作为键。 This produces the following crazy call signature type:这会产生以下疯狂的调用签名类型:

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

But it works:但它有效:

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

And this time, if you change the implementation, the output type will change accordingly:而这一次,如果您更改实现,输出类型将相应更改:

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

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

So the compiler really is verifying types for you.所以编译器真的在为你验证类型。 Hooray!万岁!

But at what cost?但代价是什么? I wouldn't want to see code like this in any production environment;我不想在任何生产环境中看到这样的代码; overloads or even generic conditionals with type assertions are much more reasonable, even though they aren't as type safe in the implementation.重载或什至带有类型断言的通用条件更合理,即使它们在实现中不是类型安全的。 Type safety isn't always more important than idiomatic coding style.类型安全并不总是比惯用的编码风格更重要。

Playground link to code 游乐场代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM