我如何 return 传递给函数的相同类型?

How do I return the same type that is passed into a function?

我有一个函数可以接收类型为 TLocation{height: number, width: number} 的值。

TLocation定义如下:

type TLocation = {
  height: number,
  width: number,
  x: number,
  y: number
}

这是我的函数:

function transformer<
  L: { height: number, width: number } | TLocation,
  V: { height: number, width: number }
>(location: L, viewbox: V): L {
  if (location.hasOwnProperty('height')) {
    return {
      height: location.height / viewbox.height,
      width: location.width / viewbox.width,
      x: location.x / viewbox.width,
      y: location.y / viewbox.height
    };
  } else {
    return {
      height: location.height / viewbox.height,
      width: location.width / viewbox.width
    };
  }
}

这是我遇到流程错误的地方的屏幕截图:

两个错误是:

Flow: Cannot return object literal because object literal [1] is incompatible with `L` [2].

我知道我不能像我尝试的那样使用 hasOwnProperty,但我不确定如何正确 return 正确的值(并因此键入)。

如果在调用该函数时我需要做任何特别的事情,我也非常希望能提供一个用法示例。

简短的回答是,不,你不能用心流来做到这一点,而不是以你想要的方式。

从根本上说,您有两个不同的主要事情正在进行。您遇到类型细化问题,并且 return 泛型类型存在问题。

类型优化

类型细化是 the official flow docs 确实不足的领域。如果他们清楚目前不能通过类型优化完成什么,那将会更有用。真的很简单。

你基本上有两种对象类型,AB,但你知道 A 有一个成员 B 没有,所以你应该能够根据该成员是否存在将对象细化为 AB。这对你来说很有意义,而且似乎应该让流动变得有意义。但是 Flow 没有为此实现机制。

type A = {| +aMember: string |};

type B = {| +bMember: string |};

// `thing` is a union of `A` and `B` who have different properties
declare var thing: A | B;

// Only `A` has `aMember`, so we should be able to check for that
// to refine the type, right?
if (typeof thing.aMember === 'string') {
  (thing: A); // ERROR! `thing` is still a union of `A` and `B`!
}

(try)

不幸的是,流程中的类型细化有点太天真了,无法解决这个问题。这个特殊的检查 完成的是它改进了 属性:

的类型
if (typeof thing.aMember === 'string') {
  // we can now use `thing.aMember` as a string just fine, but
  // `thing` is still a union of `A` and `B`
  (thing.aMember: string);
}

(try)

我们可以在流程中将整个对象细化为不同类型的唯一方法是使用 disjoint union。这意味着我们将引入一个文字字段来区分两种对象类型,这可能不是您在这种情况下想要做的:

type A = {| type: 'a', +aMember: string |};

type B = {| type: 'b', +bMember: string |};

// `thing` is a union of `A` and `B` who have different properties
declare var thing: A | B;

if (thing.type === 'a') {
  // flow now knows that `thing` is of type `A`
  (thing.aMember: string);
} else {
  // and here in the else, flow knows that `thing` is of type `B`
  (thing.bMember: string);
}

(try)

通用Return类型

那么如果我们决定使用不相交的并集来解决这个问题会怎样呢?

type Common = {| width: number, height: number |};

type Size = {| ...Common, type: 'size' |};

type Rect = {| ...Common, type: 'rect', x: number, y: number |};

const func = <T: Size | Rect>(thing: T): T => {
  // Here we've bypassed the type refinement problem by using a
  // disjoint union, but we're not returning `thing`, are we?
  if (thing.type === 'size') {
    // ERROR!
    // Cannot return object literal because object literal [1] 
    // is incompatible with `T` [2].
    return { type: 'size', width: 0, height: 0};
  } else {
    // ERROR!
    // Cannot return object literal because object literal [1] 
    // is incompatible with `T` [2].
    return {
      type: 'rect',
      width: 0,
      height: 0,
      x: 0,
      y: 0,
    }
  }
}

(try)

问题是我们的函数期望 return 恰好 T,但我们不是 returning T,我们是 return荷兰国际集团一些其他类型。你可能会想,“我们要 return 一些 T 在它被 returned 时兼容的类型,”但这实际上不是我们告诉流的.我们告诉流程期望我们 return 正好 T.

让我们看一个更简单的例子:

const f = <T: number | string>(thing: T): T => {
  if (typeof thing === 'number') {
    return 0;
    // ERROR!
    // Cannot return `0` because number [1] is incompatible 
    // with `T` [2].
  } else {
    return 'stuff';
    // ERROR!
    // Cannot return `stuff` because string [1] is incompatible 
    // with `T` [2].
  }
}

(try)

为什么这不起作用?好吧,我认为考虑这个问题的最佳心智模型是模板。让我们考虑泛型类型的工作方式是为泛型类型边界定义的集合的每个成员创建函数的版本。这显然不是实际的工作方式,但在考虑这个示例时会很有用。

在这种情况下,我们的泛型定义了一组两种类型,stringnumber,因此让我们考虑一下这会导致此函数的两个版本:

const fForNumber = (thing: number): number => {
  if (typeof thing === 'number') {
    return 0;
    // Works fine.
  } else {
    return 'stuff';
    // ERROR!
    // Cannot return `stuff` because string [1] is incompatible 
    // with `number` [2].
  }
}

const fForString = (thing: string): string => {
  if (typeof thing === 'number') {
    return 0;
    // Cannot return `0` because number [1] is incompatible 
    // with string [2].
  } else {
    return 'stuff';
    // Works fine.
  }
}

(try)

所以基本上我们只是复制并粘贴了函数,然后在第一个函数的情况下将 T 替换为 number,我们将其替换为 string在第二种情况下。独立查看这些版本中的每一个时,很清楚为什么它们会出错。现在我们只需要考虑带有泛型的版本基本上就是这两个函数叠加在一起,结果就是我们都得到了错误。为了不在我们的泛型函数中出现任何错误,泛型边界定义的每个类型都必须在函数的每条路径中独立地代替泛型。

解决方案

鉴于此公式,最直接的解决方案是基本上忽略错误:

type TLocation = {
  height: number,
  width: number,
  x: number,
  y: number
}

function transformer<
  L: { height: number, width: number } | TLocation,
  V: { height: number, width: number }
>(location: L, viewbox: V): L {
  // Refine both of these properties so we can use them without errors:
  if (typeof location.x === 'number' && typeof location.y === 'number') {
    return (({
      height: location.height / viewbox.height,
      width: location.width / viewbox.width,
      x: location.x / viewbox.width,
      y: location.y / viewbox.height
      // Type through `any` to `L`
    }: any): L);
  } else {
    return (({
      height: location.height / viewbox.height,
      width: location.width / viewbox.width
      // Type through `any` to `L`
    }: any): L);
  }
}

还可以 因为我们(作者,不是流)知道我们应该 returning 是什么类型,所以只要我们是return如果正确,它应该可以正常工作。但显然这有点丑陋,因为我们基本上只是关闭了类型系统。不幸的是,我们将不得不在某个地方做出妥协以使这个特定的公式起作用。

通常在这种情况下,备份和实施更明确的设计最有意义:

type Size = {| width: number, height: number |};

type Rect = {| ...Size, x: number, y: number |};

function sizeTransformer(size: Size, viewbox: Size): Size {
  return {
    height: size.height / viewbox.height,
    width: size.width / viewbox.width,
  }
}

function rectTransformer(rect: Rect, viewbox: Size): Rect {
  return {
    ...sizeTransformer({ width: rect.width, height: rect.height }, viewbox),
    x: rect.x / viewbox.width,
    y: rect.y / viewbox.height
  }
}

(try)

显然,您必须考虑当前的使用方式,但通常有一种方法可以实现更具体的解决方案。

我认为人们在开始使用强类型代码时倾向于做的一件事是尝试将强类型融入他们习惯于来自更动态环境的模式。根据我的经验,这通常是一种代码味道。它通常表明可以从数据优先的角度更多地考虑代码。泛型是一个强大而强大的工具,但如果您问自己是否可以使用它们来解决一个非常小的用例,那么现在可能是退后一步并重新考虑该方法的好时机。