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

时间:2018-04-06 02:52:35

标签: typescript nullable 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;
    }
}

<小时/> 背景:我通常使用一个函数来检查我的类的不变量;例如,假设我有一个带有一些可空字段的类,它们被异步初始化。

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

此类经历复杂的异步初始化过程,之后dbConnection变为非null。在初始化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 ,因为这个功能通常更复杂。

我唯一知道的技巧是创建一个与当前类具有相同字段的接口,但是使用不可为空的类型(没有?)。然后我可以做一个说this is InitializedDbClient的打样。不幸的是,这需要复制类定义的大部分内容。还有更好的方法吗?

2 个答案:

答案 0 :(得分:3)

是的,你可以你几乎完全正确地使用伪代码

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

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

注意您的伪代码and如何成为&。非常接近。

当然,在这种情况下,没有必要使用该运算符,类型为交集运算符,因为我们可以将其简化为

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')) {...}

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

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

答案 1 :(得分:1)

在Typescript中声明类属性后,无法更改类属性的类型。您使用类型断言函数在本地重新限定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();