[英]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);
因此,如果withError
为true
,则B
类型参数将为true
,如果withError
为false
或被省略,则为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
)。 但是我们可以使用模板文字将true
、 false
或undefined
转换为字符串文字"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.