Сужение универсального свойства union интерфейса c, как если бы оно было локальной переменной в машинописи - PullRequest
2 голосов
/ 18 января 2020

У меня есть объект данных, который мне нужно передать в одно из двух мест, в зависимости от того, какой член объединения содержит данный объект. В обоих местах нужны все данные объекта, поэтому воссоздание объекта суженного типа выглядит немного глупо. Хотя, очевидно, это работает.

В качестве альтернативы я попытался сделать интерфейс объекта generi c поверх объединения, чтобы интерфейс мог представлять объект во всех трех местах, полагая, что может применяться автоматическое сужение типа c. к параметру типа TestData.

interface UserData {
    kind: 'user',
    user: string,
}

interface ServerData {
    kind: 'server',
    url: string,
}

type DataTypes = UserData | ServerData

interface TestData<D extends DataTypes> {
    data: D,
    id: string,
}

Теперь высший класс может использовать TestData<DataTypes>, а дети могут использовать TestData<UserData> или TestData<ServerData>. Это работает нормально, пока вы не попытаетесь передать объект одному из детей. Компилятор правильно сузит свойство TestData data, но это не сужает тип реального объекта, который все еще имеет тип TestData<DataTypes>. Вот пример.

function basicNarrow(test: TestData<DataTypes>) {
    if (test.data.kind === 'user') {
        // Correctly narrowed to `UserData`
        test.data.user 
        // Error: Generic type not narrowed, still `TestData<DataTypes>
        const typed: TestData<UserData> = test 
    } else {
        // Correctly narrowed to `ServerData`
        test.data.url 
        // Error: Generic type not narrowed, still `TestData<DataTypes>
        const typed: TestData<ServerData> = test 
    }
}

В этот момент я мог либо использовать утверждение типа, либо (снова) создать новый объект для передачи правильного типа, но после некоторого поиска я нашел этот ответ для аналогичный вопрос , который дает мне

type NarrowKind<T, N> = T extends { kind: N } ? T : never;

function predicateNarrow(test: TestData<DataTypes>) {
    const predicate = <K extends DataTypes['kind']>(narrow: TestData<DataTypes>, kind: K): narrow is TestData<NarrowKind<DataTypes, K>> => (
        narrow.data.kind === kind
    )

    if (predicate(test, 'user')) {
        // Correctly narrowed to `UserData`
        test.data.user 
        // Success! Generic type narrowed to `TestData<UserData>
        const typed: TestData<UserData> = test 
    } else {
        // Error: Not narrowed
        test.data.url 
        // Error: Generic type not narrowed, still `TestData<DataTypes>
        const typed: TestData<ServerData> = test 
    }
}

Это делает то, что я делал после того, как внутри блока if, но компилятор не сузится до альтернативного случая в блоке else без другого явная проверка, как если бы data была просто локальной переменной.

Вот пример того, что я хотел бы, чтобы суженные типы были в идеале

function idealNarrow(test: TestData<DataTypes>) {
    function isKind(/*???*/) { /*???*/ }

    if (isKind(test, 'user')) {
        const user: UserData = test.data 
        const typed: TestData<UserData> = test 
    } else {
        const server: ServerData = test.data 
        const typed: TestData<ServerData> = test 
    }
}

Любой из решения можно использовать без проблем, но predicateNarrow(...) - это , поэтому близко к тому, что я искал, есть ли способ как-то объединить эти два поведения, чтобы сузить весь тип generi c TestData<D> автоматически в блоке else?

1 Ответ

1 голос
/ 18 января 2020

Проблема здесь в том, что TestData само по себе не является распознаваемым типом объединения , только D содержащегося data свойства. Другими словами, TS может сузить data на kind дискриминант, а не внешний тип TestData.

predicate может проверять только TestData на наличие указанного c типа UserData или ServerData, но он не может выводить другие возможные части союзов с потоком управления в блоке if/else. Возможные решения:

1) Узкий DataTypes и рекомбинация TestData ( код )

function basicNarrow({ id, data }: TestData<DataTypes>) {
    if (data.kind === 'user') {
        data // UserData
        const typed: TestData<UserData> = { id, data }
    } else {
        data // ServerData
        const typed: TestData<ServerData> = { id, data }
    }
}

2) Сделайте TestData самим различимым объединением ( код )

type DataTypes = UserData | ServerData
type TestData<D extends DataTypes> = D & { id: string }

function basicNarrow(test: TestData<DataTypes>) {
    if (test.kind === 'user') {
        test // UserData & { id: string; }
        const typed: TestData<UserData> = test
    } else {
        test // ServerData & { id: string; }
        const typed: TestData<ServerData> = test
    }
}
...