- 原文地址:Using Typescript to make invalid states irrepresentable
- 原文做者:Javier Casas
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:solerji
- 校对者:lgh757079506,lsvih
有一种好的 Haskell 编程原则,一样也是一种好的函数式编程原则,叫作使无效状态不可恢复原则。这是什么原则呢?一般咱们使用类型系统来构建对数据和状态施加约束的类型,从而达到能够表明已存在状态的效果。如今,在类型级别上,咱们设法消除了无效状态,但类型系统每次试图构造无效状态时都会介入无效状态,并给咱们带来麻烦。若是咱们不能构造一个无效的状态,咱们的程序就很难以无效的状态结束,由于为了达到无效的状态,程序必须遵循一系列构造无效状态的操做。可是这样的程序在类型级别上是无效的,排版检查程序会告诉咱们:咱们作了一些错误的事情。这很棒,因为类型系统会为咱们记住数据的约束条件,因此咱们没必要依赖爱忘事的内存来记住它们。前端
幸运的是,这项技术的许多结果能够应用于其余编程语言,今天咱们将在 Typescript 中进行试验。android
让咱们来研究一个示例问题,这样咱们就能够尝试理解如何使用它。咱们将使用代数数据类型来约束一个函数的类型,这样咱们就能够防止对它使用无效参数。咱们的小例子以下:ios
field1
和 field2
。field1
,没有 field2
。field1
时,它才能有 field2
。field2
的对象而没有 field1
的对象无效。field1
或 field2
时,它们将是 string
类型,但它们自己能够是任何类型的。 让咱们从最简单的方法开始。因为 field1
和 field2
均可以存在,或者不存在,因此咱们只是让它们成为可选的。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!"})复制代码
因此咱们连续几回在一行中用错误的字段调用 receiver
,咱们的应用程序就会出问题。我么彷佛该作些什么了。让咱们再看一下这些示例,以便咱们能够查看是否能够生成正确的类型:typescript
field1
,不能有 field2
。field1
时,它才能有 field2
。所以,在本例中,对象同时具备 field1
和 field2
。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
,类型为 string
的 object
都是可行的,除了咱们建立新对象的状况,例如:
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 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。