首页 > 解决方案 > 如何确保只读属性不可分配给可变属性?

问题描述

假设我在 TypeScript 中有两种类型,A并且B希望类型A的值可以分配给B.

我想强制执行这一点,这样如果一个(或两个)类型被意外修改以允许分配,我们会得到编译错误或测试失败。有没有办法在 TypeScript 中实现这一点?

当然,只要在测试中进行分配,就很容易强制执行分配合法的。但我无法立即看到强制特定代码不通过TypeScript 类型检查的方法。

以下是有关我为什么要这样做的更多背景信息。我想强制执行特定类型的不变性,如下所示:

interface MyImmutableThing {
    readonly myfield: string;
}

但是,这个问题会造成问题,因为如果我有一个像这样的其他相同的可变类型:

interface MyThing {
    myfield: string;
}

然后 typeMyImmutableThing的值可以分配给MyThing,从而允许绕过和改变类型安全myfield。以下代码编译、运行并导致imm.myfield更改:

const imm: MyImmutableThing = {myfield: 'mumble'};
const mut: MyThing = imm;
mut.myfield = 'something else';

我不知道如何在编译时稳健地确保此类类型的不变性,但我至少可以通过使用类来实现运行时强制,如下所示:

class MyImmutableThing {
    private _myfield: string;
    get myfield(): string { return this._myfield; }
    constructor(f: string) { this._myfield = f; }
}

然后,虽然像下面这样的代码仍然可以编译,但它会导致运行时错误:

const imm = new MyImmutableThing('mumble');
const mut: MyThing = imm;
mut.myfield = 'something else';

然后我可以编写一个测试来断言发生此运行时错误。

但是,如果我的字段是数组(或元组)类型,情况会发生变化:

interface MyArrayThing {
    myfield: string[];
}
interface MyImmutableArrayThing {
    readonly myfield: readonly string[];
}

现在,由于数组类型的特性, 的值MyImmutableArrayThing不能分配给。以下将无法编译:MyArrayThingreadonly

const imm: MyImmutableArrayThing = {myfield: ['thing']};
const mut: MyArrayThing = imm;

这很好,因为它为我们提供了比我们在该string领域获得的更多的编译时不变性保证。然而,现在更难编写测试来捕捉我们的意图,或者以其他方式强制执行它。

MyImmutableArrayThings to的不可分配性MyArrayThing是强制执行我们想要的属性的类型系统的关键,但是我们如何阻止某人进行一些更改,例如添加readonly到数组 in MyArrayThing,允许这样的事情并破坏我们想要的属性?

interface MyArrayThing {
    myfield: readonly string[]; // now readonly
}
interface MyImmutableArrayThing {
    readonly myfield: readonly string[];
}
const imm: MyImmutableArrayThing = {myfield: ['thing']};
const mut: MyArrayThing = imm;
mut.myfield = ['other thing'];

目前, TypeScript 的readonly执行相当混乱,因此能够做出这种断言将非常有助于防止回归。

这是此问题中代码的 TypeScript Playground 链接

标签: typescript

解决方案


通过使用一种称为“类型标记”的技术,您可以实现“名义类型”,即尽管共享相同的结构,但使相似类型不兼容。

请查看TypeScript 游乐场中的此示例以了解更多详细信息。

在您的情况下,类型品牌可能如下所示:

interface ImmutableThing {
  readonly myfield: string
  __brand: "ImmutableThing"
}

interface MutableThing {
  myfield: string
  __brand: "MutableThing"
}

const imm: ImmutableThing = {myfield: "thing"} as ImmutableThing;
const mut: MutableThing = imm; // type error
mut.myfield = "mutated"; 

游乐场链接

如果您对类型品牌感兴趣,请查看ts-brand了解更高级的用法。


推荐阅读