如何在TypeScript中创建密封的抽象类



在Kotlin中,密封类是一个抽象类,其直接子类在编译时是已知的。密封类的所有直接子类必须与密封类在同一模块中定义。任何其他模块中定义的类都不能扩展密封类。这允许Kotlin编译器对密封类执行穷尽性检查,就像TypeScript编译器对并集所做的那样。我想知道是否有可能在TypeScript中实现类似的东西。

考虑这个抽象类Expr及其直接子类NumAdd

abstract class Expr<A> {
public abstract eval(): A;
}
class Num extends Expr<number> {
public constructor(public num: number) {
super();
}
public override eval() {
return this.num;
}
}
class Add extends Expr<number> {
public constructor(public left: Expr<number>, public right: Expr<number>) {
super();
}
public override eval() {
return this.left.eval() + this.right.eval();
}
}

下面是Expr类的一个示例实例。

// (1 + ((2 + 3) + 4)) + 5
const expr: Expr<number> = new Add(
new Add(new Num(1), new Add(new Add(new Num(2), new Num(3)), new Num(4))),
new Num(5)
);

我想将此实例转换为右关联表达式。

// 1 + (2 + (3 + (4 + 5)))
const expr: Expr<number> = new Add(
new Num(1),
new Add(new Num(2), new Add(new Num(3), new Add(new Num(4), new Num(5))))
);

因此,我在Expr类中添加了一个rightAssoc抽象方法。

abstract class Expr<A> {
public abstract eval(): A;
public abstract rightAssoc(): Expr<A>;
}

并在NumAdd两个子类中实现了该方法。

class Num extends Expr<number> {
public constructor(public num: number) {
super();
}
public override eval() {
return this.num;
}
public override rightAssoc(): Num {
return new Num(this.num);
}
}
class Add extends Expr<number> {
public constructor(public left: Expr<number>, public right: Expr<number>) {
super();
}
public override eval() {
return this.left.eval() + this.right.eval();
}
public override rightAssoc(): Add {
const expr = this.left.rightAssoc();
if (expr instanceof Num) return new Add(expr, this.right.rightAssoc());
if (expr instanceof Add) {
return new Add(expr.left, new Add(expr.right, this.right).rightAssoc());
}
throw new Error('patterns exhausted');
}
}

这是意料之中的事。然而,它有一个问题。在Add#rightAssoc方法的实现中,如果expr既不是Num的实例,也不是Add的实例,我将抛出一个错误。现在,假设我创建了Expr的一个新子类。

class Neg extends Expr<number> {
public constructor(public expr: Expr<number>) {
super();
}
public override eval() {
return -this.expr.eval();
}
public override rightAssoc(): Neg {
return new Neg(this.expr.rightAssoc());
}
}

TypeScript没有抱怨Add#rightAssoc中的一系列instanceof检查不是详尽无遗的。因此,当exprNeg的实例时,我们可能会意外地忘记实现这种情况。有没有什么方法可以在TypeScript中模拟密封类,以便检查instanceof检查的穷尽性?

TypeScript中没有与此完全等效的内容。你将得到的最接近的东西,是受歧视的工会

// Base class
export abstract class ExprBase<A> {
abstract type: string
public abstract eval(): A;
public abstract rightAssoc(): Expr;
}
// Discriminated unbion
type Expr = Num | Add | Neg;
class Num extends ExprBase<number> {
type = "num" as const;
public constructor(public num: number) {
super();
}
public override eval() {
return this.num;
}
public override rightAssoc(): Num {
return new Num(this.num);
}

}
// Function that ensures compiler error if the union is not checked exhaustively
function assertNever(a: never): never {
throw new Error('patterns exhausted');
}
class Add extends ExprBase<number> {
type = "add" as const;
public constructor(public left: Expr, public right: Expr) {
super();
}
public override eval(): number {
return this.left.eval() + this.right.eval();
}
public override rightAssoc(): Add {
const expr = this.left.rightAssoc();
if (expr.type === "num") return new Add(expr, this.right.rightAssoc());
if (expr.type === "add") {
return new Add(expr.left, new Add(expr.right, this.right).rightAssoc());
}
// Error now, expr is Neg
assertNever(expr);
}
}
class Neg extends ExprBase<number> {
type = "neg" as const;
public constructor(public expr: Expr) {
super();
}
public override eval(): number {
return -this.expr.eval();
}
public override rightAssoc(): Neg {
return new Neg(this.expr.rightAssoc());
}
}

游乐场链接

我找到了一种方法来进行详尽的模式匹配,并防止新类扩展密封类。首先,我们需要在密封类Expr中定义一个新的抽象方法match

abstract class Expr<A> {
public abstract match<B>(which: {
Num: (expr: Num) => B;
Add: (expr: Add) => B;
}): B;
public abstract eval(): A;
public abstract rightAssoc(): Expr<A>;
}

match方法允许您对一组已知的直接子类进行模式匹配。因此,match方法对于这些直接子类的实现是微不足道的。例如,下面是Num#match方法的实现。

class Num extends Expr<number> {
public constructor(public num: number) {
super();
}
public override match<B>(which: {
Num: (num: Num) => B;
}): B {
return which.Num(this);
}
public override eval() {
return this.num;
}
public override rightAssoc(): Num {
return new Num(this.num);
}
}

类似地,我们可以实现Add#match方法。此外,我们可以使用这个match方法来实现Add#rightAssoc方法。TypeScript确保我们始终处理所有案例。

class Add extends Expr<number> {
public constructor(public left: Expr<number>, public right: Expr<number>) {
super();
}
public override match<B>(which: {
Add: (expr: Add) => B;
}): B {
return which.Add(this);
}
public override eval() {
return this.left.eval() + this.right.eval();
}
public override rightAssoc(): Add {
return this.left.rightAssoc().match({
Num: (expr) => new Add(expr, this.right.rightAssoc()),
Add: (expr) => new Add(
expr.left,
new Add(expr.right, this.right).rightAssoc()
)
});
}
}

如果我们试图添加一个新的子类,那么我们将不得不实现match方法。这将迫使我们更新密封类中match方法的定义。这反过来会迫使我们更新使用match方法处理新案例的每个地方。

使match方法抽象也使得创建Expr的子类变得困难。为了实现match方法,我们需要调用已知直接子类的一个处理程序。因此,对于未知的新子类,match方法在语义上是不正确的。

最新更新