我可以编写断言多个不变量的类型保护吗?

Can I write a type guard that asserts multiple invariants?

我可以写一个类型保护断言关于一个或多个子对象的参数吗?在伪代码中,它可能看起来像这样:

class C {
    a: number?;
    b: string?;

    function assertInitialized() : (this.a is number) and (this.b is string) {
        return this.a !== null && this.b !== null;
    }
}


背景: 我通常使用单个函数来检查 classes 的不变量;例如,假设我有一个 class 和一些可以为 null 的字段,这些字段被异步初始化。

class DbClient {
    dbUrl: string;
    dbName: string;
    dbConnection: DBConnection?;
    …
}

这个class经历了一个复杂的异步初始化过程,之后dbConnection变成了非空。 class 的用户在 dbConnection 初始化之前不能调用某些方法,所以我有一个函数 assertReady:

assertReady() {
    if (this.dbConnection === null) {
        throw "Connection not yet established!";
    }
}

这个函数assertReady在每个需要DbClient完全初始化的函数的开头调用,但我还是要写非空断言:

fetchRecord(k: string) {
    assertReady();
    return this.dbConnection!.makeRequest(/* some request based on k */);
}

我可以给 assertReady 一个签名,使 ! 变得不必要吗? 我不想将 this.dbConnection 传递给 assertReady,因为该函数通常更复杂。

我知道的唯一技巧是创建一个接口,该接口具有与当前 class 相同的字段,但具有不可为 null 的类型(没有 ?)。然后我可以制作一个表示 this is InitializedDbClient 的类型保护。不幸的是,这需要复制大部分 class 定义。有没有更好的方法?

在 Typescript 中声明 class 属性 后,您无法更改它的类型。您关于在本地 re-qualify this 使用类型断言函数的建议可能有一些优点:

interface DefinitelyHasFoo
{
    foo: number;
}

class MaybeHasFoo
{

    public foo: number | null;

    constructor()
    {
        this.foo = null;
    }

    public hasFoo(): this is DefinitelyHasFoo
    {
        return !!(this.foo !== null);
    }

    public doThing()
    {
        if (this.hasFoo())
        {
            this.foo.toExponential();
        }
        else
        {
            throw new Error("No foo for you!");
        }

    }
}

// ...

const mhf = new MaybeHasFoo();

mhf.doThing();

是的,你可以并且你在伪代码中几乎完全正确

Interface A {
  a?: number;
  b?: string;

  hasAandB(): this is {a: number} & {b: string};
}

注意你的伪代码 and 是如何变成 & 的。确实非常接近。

当然没必要用那个运算符,类型intersection运算符,在这种情况下,因为我们可以将它简化为

hasAandB(): this is {a: number, b: string};

但是假设我们添加第三个 属性,比如 c,它不受类型保护的影响,但我们不想失去它对结果类型的所有贡献一样。

你对可组合类型守卫的直觉让我们回到原点

hasAandB(): this is this & {a: number, b: string};

您可以使用这些模式做各种非常有趣且非常有用的事情。

例如,您可以根据对象的类型一般地传递​​许多 属性 键,并且根据您传递的实际键,结果可以类型保护到承载的交集每个 属性.

function hasProperties<T, K1 extends keyof T, K2 extends keyof T>(
  x: Partial<T>,
  key1: K1,
  key2: K2
): x is Partial<T> & {[P in K1 | K2]: T[P]} {
  return key1 in x && key2 in x;
}


interface I {
  a: string;
  b: number;
  c: boolean;
}

declare let x: Partial<I>;

if (hasProperties(x, 'a', 'b')) {...}

实际上,这只是触及了可能性的表面。

另一个非常有趣的应用是定义任意通用和类型安全的构建器和可组合工厂。