typescript - 如何使用函数式组合样式构建 Typescript 对象

标签 typescript functional-programming

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

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

例如,在下面的代码中,我从一个 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 无法运行的类型。

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

最佳答案

我认为你的主要问题是你正在编写的函数需要是 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 limitationalgorithm that supports higher-order generic function inference介绍于 TypeScript 3.4 .当类型在函数参数中从左向右流动时,该算法表现相当好,而当类型应该从右向左流动时,该算法表现相当差。在这种情况下,您将需要放弃类型推断并在某处手动指定类型。例如,给定此处的 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

关于typescript - 如何使用函数式组合样式构建 Typescript 对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57475557/

相关文章:

c++ - 需要帮助将函数从 C++ 移植到 Ocaml

Angular 5 : Iterate Over Enum Using *ngFor

java - 将一系列整数分组为函数的答案

java - 意外的实例方法函数...在未绑定(bind)查找中找到

typescript - 如何访问可选属性的 "type"属性?

java - 是否有可能在 Stream 中获取下一个元素?

scala - 用Scala中的占位符替换字符串中的值

typescript - 理解 typescript 类型断言

angular - JHipster:Angular/TypeScript:console.log 导致编译错误

javascript - 方法参数声明中的 "colon"表示法之后可能会出现什么情况?