在JavaScript中寻找不可变的,类型安全的数据记录

自从与Scala合作以来案例类我迷上了拥有一个不可变的类型安全数据记录的想法What's not to like? It's type safe不可变的(duh)所以我想知道我是否可以在JavaScript中获得相同的东西 - 人类已知的最易变和动态的语言。

class Person {  
  givenName;
  familyName;
}

这将作为我们的起点:JavaScript中的一个简单类它包含两个类实例字段ES类字段和静态属性ES7提案此外,它是可变的经过一个实例已创建,其字段可以更改。

immutable.js

Facebook的JavaScript库一成不变提供不可改变的记录数据类型让我们看看它需要多远。

const Person = Record({givenName:'',familyName:''});

这就是我们定义记录的方式如果你问我,为了定义记录结构,需要提供默认值似乎很奇怪它混合了两个问题:结构定义和默认值Consequently, we can't require a property to be provided; there is always a default value.

无论如何,我们可以直接访问每个属性并且不会被迫使用类似的东西。得到()

const dad = new Person({ givenName: 'Homer', familyName: 'Simpson' });  
dad.givenName // 'Homer'

非常令人惊讶的是,在构造具有任何其他属性的记录时没有运行时错误。

const dad = new Person({ givenName: 'Homer', age: 40 });  
dad.age // undefined

我觉得这很危险也许我们可以通过添加类型安全来减少问题?

在JavaScript中输入安全性?目前有两项认真努力为JavaScript带来类型安全:打字稿从我到目前为止看到的,它们的语法几乎相同。

我选择了Flow,因为它似乎更容易上手,并且也适用于BabelTypeScript可能也可以。

有一个未决问题有适当的流类型定义一成不变但是使用它当前的状态已经很好了。

const dad = new Person({ givenName: 42 });  
// type error: number is incompatible with string

const dad = new Person({ givenName: 'Homer', age: 42 });  
// type error: property `age` not found

That's quite nice! We get checks for wrong data types and unknown initialisation properties.

不幸的是,似乎没有正确检查属性访问。

dad.age  
// type checks OK, returns undefined

总而言之,我们可以走得很远记录但它并不完美。

巴别塔

看来我们无法通过编写代码达到目标,我们需要转换代码由于JavaScript没有宏来做到这一点,我们采取了下一个最好的事情:一个Babel插件。

什么是巴别塔? 巴别塔作为一个转换器出生,它采用现代JavaScript代码并发出代码,可以在较旧的平台上运行,其中一些最新功能尚不支持但它已经发展成为更通用的代码转换和代码生成平台。

插件需要知道哪个类是不可变的,哪些类要忽略所以我们创建一个新的装饰器来标记转换类:

@Record()
class Person {  
  givenName;
  familyName;
}

我们的Babel插件会寻找@记录并将代码转换为:

@Record()
class Person {  
  constructor(init) {
    this.__givenName = init.givenName;
    this.__familyName = init.familyName;
  }

  __givenName;
  __familyName;

  get givenName() {
    return this.__givenName;
  }

  get familyName() {
    return this.__familyName;
  }
}

这是发生的事情:

  • 这些字段被重命名为“私有”
  • 创建getter以仅允许读取访问
  • 添加了一个构造函数来初始化字段

这就是我们如何使用它:

const dad = new Person({ givenName: 'Homer', familyName: 'Simpson' });  
console.log(`created dad ${dad.givenName} ${dad.familyName}`);  
// will output "created dad Homer Simpson"

但它还不是非常有用How do we change it? By creating a copy! Since this can be quite cumbersome and error-prone for larger objects, we generate a method to help us with that:

@Record()
class Person {  
  // ...

  update(update) {
    return new Person({
      givenName: update.givenName || this.__givenName,
      familyName: update.familyName || this.__familyName
    });
  }
}

更新获取一个对象并创建一个新对象通过使用提供的数据或回退到现有数据,如果没有提供。

现在我们可以轻松创建我们的记录副本:

const son = dad.update({ givenName: 'Bart' });  
console.log(`created son ${son.givenName} ${son.familyName}`);  
// will output "created son Bart Simpson"

下一步是使其类型安全基本上,我们的原始代码只接收其字段的类型注释而已。

@Record()
class Person {  
  givenName: string;
  familyName: string;
}

该插件现在主要只是将新类型的注释复制到正确的位置,但也创建了两种新类型:

@Record()
class Person {  
  constructor(init: PersonInit) {
    this.__givenName = init.givenName;
    this.__familyName = init.familyName;
  }

  __givenName: string;
  __familyName: string;

  get givenName(): string {
    return this.__givenName;
  }

  get familyName(): string {
    return this.__familyName;
  }

  update(update: PersonUpdate): Person {
    return new Person({
      givenName: update.givenName || this.__givenName,
      familyName: update.familyName || this.__familyName
    });
  }
}

type PersonInit = {  
  givenName: string;
  familyName: string;
};

type PersonUpdate = {  
  givenName?: string;
  familyName?: string;
};

第一个,PersonInit,定义用于初始化记录的对象的类型第二个,PersonUpdate,定义用于创建记录副本的对象的类型重要的是要注意它包含可选属性(标记为在末尾)这意味着客户端不必指定其中任何一个。

我们现在有一个类型安全,不可变的记录。

new Person({ givenName: 'Homer', familyName: 'Simpson' });  
// OK

new Person({ givenName: 'Homer' });  
// type error: property `familyName` not found

new Person({ givenName: 'Homer', familyName: true });  
// type error: boolean is incompatible with string

new Person({ givenName: 'Homer', familyName: 'Simpson' }).update({});  
// OK

不幸的是,类型检查器在初始化或更新期间提供未知属性时失败这意味着我们可以轻松地为可选字段输入拼写错误并想知道为什么没有发生任何事情。

const daughter = dad.update({givnam: 'Lisa'};  
// OK

显然,这是Flow中的一个已知限制该解决方法是的密封通过添加'catch-all属性'来添加对象类型空虚类型。

type PersonInit = {  
  givenName: string;
  familyName: string;
  [key: string]: void;
};

type PersonUpdate = {  
  givenName?: string;
  familyName?: string;
  [key: string]: void;
};

现在,当我们使用无效的属性键时,它会失败。

const daughter = dad.update({givnam: 'Lisa'};  
// type error: string is incompatible with undefined

这个承认有点神秘的错误信息现在告诉我们givnam不属于更新数据类型。

顺便说一句,要使这种类型检查Flow配置文件.flowconfig需要国旗unsafe.enable_getters_and_setters =真处理吸气剂。


可以在上找到此代码生成器的源代码GitHub上


评论由Disqus