Могу ли я написать защиту типа, которая утверждает несколько инвариантов?

Могу ли я написать защиту типа, утверждающую что-то об одном или нескольких подобъектах аргумента? В псевдокоде это может выглядеть так:

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

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


Предыстория: я обычно использую одну функцию для проверки инвариантов своих классов; например, предположим, что у меня есть класс с некоторыми полями, допускающими значение NULL, которые инициализируются асинхронно.

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

Этот класс проходит сложный асинхронный процесс инициализации, после которого dbConnection становится ненулевым. Пользователи класса не должны вызывать определенные методы, пока 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. К сожалению, это требует дублирования большей части определения класса. Есть ли способ лучше?


person Clément    schedule 06.04.2018    source источник
comment
Я уверен, что сейчас, наверное, уже слишком поздно, но пару дней назад я столкнулся с тем же самым. В итоге я написал прокси-класс, который автоматически ожидает готовности соединения. Единственным недостатком является то, что все запросы становятся асинхронными, но в любом случае это довольно распространено для баз данных. gist.github.com/d99b5581ad672a8a362a161550ae23c6 Я также написал синхронную версию JavaScript, которая делает ваше утверждение в этой сути, если тебе интересно.   -  person dx_over_dt    schedule 14.12.2018


Ответы (2)


Да, вы можете, и у вас почти все получилось в псевдокоде

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

И действительно, это только царапает поверхность того, что возможно.

Другое действительно интересное применение этого — определение произвольных универсальных и типобезопасных компоновщиков и компонуемых фабрик.

person Aluan Haddad    schedule 06.04.2018

Вы не можете изменить тип свойства класса после того, как объявили его в 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();
person Michael Zalla    schedule 06.04.2018
comment
Вы также можете избежать проблемы инициализации ресурсов, адаптировав свои API для использования промисов: jsfiddle.net/1b68eLdr/5144 - person Michael Zalla; 06.04.2018