【问题标题】:How to build a Typescript object using a functional compose style如何使用函数式撰写样式构建 Typescript 对象
【发布时间】:2019-08-14 23:44:35
【问题描述】:

我想使用功能组合管道来构建对象,同时保持类型安全。我应该使用哪些类型的可组合函数将它们拼接在一起?

我有一个带有一些输入数据的对象,我的目标是使用可组合函数对输入数据执行计算。

例如,在下面的代码中,我从一个 sideLength 值开始;然后用三个独立的函数添加计算值(面积、周长和立方体面积);每个人都对自己的知识负责。像 cubeArea 这样的一些计算是基于管道中先前的计算。

import * as R from "ramda";

interface ISquare {
  sideLength: number;
  area: number;
  circumference: number;
  cubeArea: number;
}
type ISquareBase = Pick<ISquare, "sideLength">;
type ISquareWithArea = Pick<ISquare, "area" | "sideLength">;
type ISquareWithCircumference = Pick<ISquare, "circumference" | "sideLength">;
type ISquareWithCubeArea = Pick<ISquare, "cubeArea" | "sideLength">;

const addArea = (shape: ISquareBase): ISquareWithArea => ({
  ...shape,
  area: shape.sideLength * shape.sideLength
});

const addCircumference = (shape: ISquareBase): ISquareWithCircumference => ({
  ...shape,
  circumference: shape.sideLength * 4
});

const addCubeArea = (shape: ISquareWithArea): ISquareWithCubeArea => ({
  ...shape,
  cubeArea: shape.area * shape.sideLength
});

const buildSquare = R.compose(
  addArea, // { sideLength: 2, area: 4 }
  addCircumference, // { sideLength: 2, area: 4, circumference: 8 }
  addCubeArea // { sideLength: 2, area: 4, circumference: 8, cubeArea: 8 }
);

const initialSquare: ISquareBase = { sideLength: 2 };

// The below line gives typescript error:
// Argument of type 'Pick<ISquare, "sideLength">' is not assignable to parameter of type 'Pick<ISquare, "sideLength" | "area">'.
// Property 'area' is missing in type 'Pick<ISquare, "sideLength">' but required in type 'Pick<ISquare, "sideLength" | "area">'.
const calculatedSquare: ISquare = buildSquare(initialSquare);

console.log(calculatedSquare);
// Expect: { sideLength: 2, circumference: 8, area: 4, cubeArea: 8 }

当我尝试使用上面的组合函数 buildSquare 时,Typescript 出现类型错误。

Argument of type 'Pick<ISquare, "sideLength">' is not assignable to parameter of type 'Pick<ISquare, "sideLength" | "area">'.
Property 'area' is missing in type 'Pick<ISquare, "sideLength">' but required in type 'Pick<ISquare, "sideLength" | "area">'.

我可以放宽类型签名以使其正常工作,但是在运行 addArea 中的面积计算之前,我无法确保 addCubeArea 无法运行的类型。

如何在保持类型安全的同时做到这一点?有没有我可以使用的替代类型结构?

【问题讨论】:

  • 顺便说一句,我会使用咖喱构造函数而不是组合。即使传递属性的顺序不同,您也可以创建自定义包装器(例如 f =&gt; c =&gt; a =&gt; d =&gt; b =&gt; f(a) (b) (c) (d) 以相应地修改构造函数..

标签: typescript functional-programming


【解决方案1】:

我认为您的主要问题是您正在编写的函数必须是generic,否则编译器无法跟踪这一事实,例如,如果sq 具有area 属性已经,addCircumference(sq) 将生成一个具有areacircumference 的对象。您当前的输入仅保证addCircumference() 的输出具有circumference,但无法指定是否存在area。就类型系统所知,也许您不会将area 复制到输出中。

首先,由于我的环境中没有 ramda 类型,我假设您使用的 compose() 行为如下:

declare namespace R {
  export function compose<I0, I1, I2, O>(
    f: (x: I0) => I1,
    g: (x: I1) => I2,
    h: (x: I2) => O
  ): (x: I0) => O;
}

现在让我们重做三个函数的签名:

const addArea = <T extends { sideLength: number }>(
  shape: T
): T & { area: number } => ({
  ...shape,
  area: shape.sideLength * shape.sideLength
});

const addCircumference = <T extends { sideLength: number }>(
  shape: T
): T & { circumference: number } => ({
  ...shape,
  circumference: shape.sideLength * 4
});

const addCubeArea = <T extends { sideLength: number; area: number }>(
  shape: T
): T & { cubeArea: number } => ({
  ...shape,
  cubeArea: shape.area * shape.sideLength
});

所有这些类型本质上都接受(适当的constrained)泛型类型T 的输入,并产生类型为T intersected 的输出,其中对象类型表示添加的属性。

这部分仍然有效:

const buildSquare = R.compose(
  addArea, // { sideLength: 2, area: 4 }
  addCircumference, // { sideLength: 2, area: 4, circumference: 8 }
  addCubeArea // { sideLength: 2, area: 4, circumference: 8, cubeArea: 8 }
);

但是现在buildSquare的类型是

const buildSquare: <T extends { sideLength: number; }>(x: T) => 
  T & { area: number; } & { circumference: number; } & { cubeArea: number; }

意味着buildSquare 接受T 类型的对象约束为{sideLength: number},并返回T 类型的对象,其中添加了三个数字属性areacircumferencenumber它。在我看来是合理的。

让我们测试一下:

const initialSquare = { sideLength: 2 };    
const calculatedSquare: ISquare = buildSquare(initialSquare); // okay

现在没有错误,万岁!好的,希望有帮助;祝你好运!

Link to code


编辑:您可能将 Ramda 的 compose() 函数倒退(正如我所说,我不确定类型)...如果是这样,那么您将很难进行类型推断。也就是说,如果R.compose 看起来像

declare namespace R {
  export function compose<I0, I1, I2, O>(
    h: (x: I2) => O,
    g: (x: I1) => I2,
    f: (x: I0) => I1
  ): (x: I0) => O;
}

那么即使你将参数的顺序切换到它,你也会得到一个错误:

const buildSquare = R.compose(
  addCubeArea, // { sideLength: 2, area: 4, circumference: 8, cubeArea: 8 }
  addCircumference, // error! { sideLength: 2, area: 4, circumference: 8 }
  addArea // { sideLength: 2, area: 4, circumference }
); // error!

这是一个已知的design limitation,在TypeScript 3.4 中引入了algorithm that supports higher-order generic function inference。当类型在函数参数中从左到右流动时,该算法表现得相当好,而当类型应该从右到左流动时则相当糟糕。在这种情况下,您将需要放弃类型推断并在某处手动指定类型。例如,给定此处的R.compose() 定义,您可以手动指定I0I1I2O 类型:

const buildSquare = R.compose<
  { sideLength: number },
  { sideLength: number; area: number },
  { sideLength: number; area: number; circumference: number },
  { sideLength: number; area: number; circumference: number; cubeArea: number }
>(
  addCubeArea, // { sideLength: 2, area: 4, circumference: 8, cubeArea: 8 }
  addCircumference, // { sideLength: 2, area: 4, circumference: 8 }
  addArea // { sideLength: 2, area: 4, circumference }
); // okay

(如果您想保存击键,您可以在该规范中使用您的类型别名)。或者您可以为R.compose() 指定每个参数的具体类型:

const addAreaConcrete = (x: { sideLength: number }) => addArea(x);
const addCircumferenceConcrete = (x: { sideLength: number; area: number }) =>
  addCircumference(x);
const addCubeAreaConcrete = (x: {
  sideLength: number;
  area: number;
  circumference: number;
}) => addCubeArea(x);
const buildSquareConcrete = R.compose(
  addCubeAreaConcrete,
  addCircumferenceConcrete,
  addAreaConcrete
);
/*
const buildSquareConcrete: (x: {
    sideLength: number;
}) => {
    sideLength: number;
    area: number;
    circumference: number;
} & {
    cubeArea: number;
}
*/

这也有效。就我个人而言,对于类型应该从右向左流动的R.compose(),我会完全放弃使用它,除非您有一些重要的原因说明您无法接受以下内容:

const buildSquareManual = <T extends { sideLength: number }>(x: T) =>
  addCubeArea(addCircumference(addArea(x))); // okay

好的,这就是我的。再次祝你好运。

Link to code with reversed compose

【讨论】:

  • 正是我需要的,谢谢。是的,我让 R.compose 绕错了方向。使用泛型类型注释切换到 R.pipe 修复它。
猜你喜欢
  • 2020-07-13
  • 2022-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-27
  • 2020-02-09
相关资源
最近更新 更多