[译]使用 Typescript 使无效状态不可恢复

      有一种好的 Haskell 编程原则,一样也是一种好的函数式编程原则,叫作使无效状态不可恢复原则。这是什么原则呢?一般咱们使用类型系统来构建对数据和状态施加约束的类型,从而达到能够表明已存在状态的效果。如今,在类型级别上,咱们设法消除了无效状态,但类型系统每次试图构造无效状态时都会介入无效状态,并给咱们带来麻烦。若是咱们不能构造一个无效的状态,咱们的程序就很难以无效的状态结束,由于为了达到无效的状态,程序必须遵循一系列构造无效状态的操做。可是这样的程序在类型级别上是无效的,排版检查程序会告诉咱们:咱们作了一些错误的事情。这很棒,因为类型系统会为咱们记住数据的约束条件,因此咱们没必要依赖爱忘事的内存来记住它们。前端

      幸运的是,这项技术的许多结果能够应用于其余编程语言,今天咱们将在 Typescript 中进行试验。android

一个例子

       让咱们来研究一个示例问题,这样咱们就能够尝试理解如何使用它。咱们将使用代数数据类型来约束一个函数的类型,这样咱们就能够防止对它使用无效参数。咱们的小例子以下:ios

  • 咱们有一个接受单个参数的函数:一个对象有两个字段,称为 field1field2
  • 对象不能同时具备这两个字段。
  • 对象可能只有 field1,没有 field2
  • 只有当对象有 field1 时,它才能有 field2
  • 所以,具备 field2 的对象而没有 field1 的对象无效。
  • 为简单起见,当存在 field1field2 时,它们将是 string 类型,但它们自己能够是任何类型的。

缺少经验的解决方案

      让咱们从最简单的方法开始。因为 field1field2 均可以存在,或者不存在,因此咱们只是让它们成为可选的。git

interface Fields {
  field1?: string;
  field2?: string;
};

function receiver(f: Fields) {
  if (f.field1 === undefined && f.field2 !== undefined) {
    throw new Error("Oh noes, this should be impossible!");
  }
  // 其余逻辑代码
}复制代码

不幸的是,这并不能阻止编译时的任何操做,还须要在运行时检查可能的错误。github

// 这不会在编译时引起任何错误
// 因此咱们必须在运行时发现它
receiver({field2: "Hahaha, I didn't put a field1!"})复制代码

基本 ADT 解决方案

       因此咱们连续几回在一行中用错误的字段调用 receiver,咱们的应用程序就会出问题。我么彷佛该作些什么了。让咱们再看一下这些示例,以便咱们能够查看是否能够生成正确的类型:typescript

  • 对象不能同时具备这两个字段。
  • 对象只能有 field1,不能有 field2
  • 只有当对象有 field1 时,它才能有 field2。所以,在本例中,对象同时具备 field1field2
  • 具备 field2 的对象无效,而不是具备 field1 的对象。

让咱们把它记录成这种类型:编程

interface NoFields {};

interface Field1Only {
  field1: string;
};

interface BothField1AndField2 {
  field1: string;
  field2: string;
};

interface InvalidObject {
  field2: string;
};复制代码

       咱们这里也包括 InvalidObject,可是写它有点傻,由于咱们不但愿它真的存在。咱们能够将其做为文档保存,或者删除它,以便进一步确认它不该该存在。如今让咱们为 Fields 字段编写一个类型:后端

type Fields = NoFields | Field1Only | BothField1AndField2;  // 我故意把放在这里的无效对象忘了复制代码

有了这种处理方式,就很难将 InvalidObject 发送给 receiver安全

receiver({field2: "Hahaha, I didn't put a field1!"});  // 类型错误!这个对象和 `Fields` 不匹配复制代码

       咱们还须要稍微调整一下 receiver 函数,主要是由于字段如今可能不存在,排版检查程序如今须要证实你将要读取的字段是否实际存在:bash

function receiver(f: Fields) {
  if ("field1" in f) {
    if ("field2" in f) {
      // 为 f.field1 和 f.field2 作些操做
    } else {
      // 为 f.field1 作些操做, 但 f.field2 不存在
    }
  } else {
    // f 是个空字段
  }
}复制代码

结构类型的限制

      不幸的是,不管好坏, Typescript 都是一个结构类型系统,若是咱们不当心的话,它会容许咱们绕过一些安全问题。 Typescript 中的 NoFields 类型(空对象、{})。在 Typescript 中,这意味着与咱们但愿它作的彻底不一样的事情。实际上,当咱们写的时候,它是这样:

interface Foo {
  field: string;
};复制代码

      Typescript 会理解任何带有 field ,类型为 stringobject 都是可行的,除了咱们建立新对象的状况,例如:

const myFoo : Foo = { field: "asdf" };  // 在这种状况下,咱们没法添加更多字段复制代码

      可是,在赋值时,将 Typescript 测试用作类型脚本,这意味着咱们的对象,可能会以咱们但愿它们具备的更多字段结束:

const getReady = { field: "asdf", unexpectedField: "hehehe" };
const myFoo : Foo = getReady;  // 这不是一个错误复制代码

      所以,当咱们将这个想法扩展到空对象 {} 时,发如今赋值时,只要该值是一个对象,而且具备所需的全部字段, Typescript 就会接受任何值。由于类型不须要字段,因此第二个条件对于任何 object 都很是成功,这彻底不是咱们想要它作的。

禁止意外字段

       让咱们试着为没有字段的对象建立一个类型,这样咱们实际上就不得不用本身的方法来愚弄类型检查器。咱们已经知道 never,这是一种永远没法知足的类型。如今咱们须要另外一种成分来表示“每个可能的领域”。这个成分是:[键:字符串]:类型。有了这两个,咱们就能够在没有字段的状况下构造对象。

type NoFields = {
  [key: string]: never;
};复制代码

      此类型表示:这是一个对象,其字段类型为 never。因为不能构造 never,没法为此对象的字段生成有效值。因此,惟一的解决方案是建立一个没有字段的对象。如今,咱们必须更加谨慎地打破这些类型:

type NoFields = {
  [key: string]: never;
};

interface Field1Only {
  field1: string;
};

interface BothField1AndField2 {
  field1: string;
  field2: string;
};

type Fields = NoFields | Field1Only | BothField1AndField2;

const broken = {field2: "asdf"};

// Bypass1: 遍历空对象类型
// Empty object is a well known code smell in Typescript
const bypass1 : {} = broken;
const brokenThroughBypass1 : Fields = bypass1;

// Bypass2: 使用 `any` 转移 hatch
// any 在 Typescript 是另外一个有名的代码 
const bypass2 : any = broken;
const brokenThroughBypass2 : Fields = bypass2;复制代码

       如今看来,咱们须要两个很是具体的步骤来破坏这个系统,这确定是很是困难的。若是咱们必须深刻地构建一个程序,咱们应该注意到一些问题。

结论

      今天,咱们看到了一种经过类型保证程序正确性的方法,它应用于一种更主流的语言:Typescript。虽然 Typescript 不能保证与 Haskell 相同的安全级别,但这并不妨碍咱们将 Haskell 的一些想法应用于 Typescript。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索