Сужение общего свойства union интерфейса, как если бы это была локальная переменная в машинописном тексте

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

В качестве альтернативы я попытался сделать интерфейс объекта универсальным для объединения, чтобы интерфейс мог представлять объект во всех трех местах, думая, что автоматическое сужение типа может применяться к параметру типа 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(...) так близок к тому, что я искал, есть ли способ каким-то образом объединить эти два поведения, чтобы автоматически сузить весь общий тип TestData<D> в else блокировать?


person Cole    schedule 18.01.2020    source источник


Ответы (1)


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

predicate может только проверить TestData на наличие определенного типа 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
    }
}
person ford04    schedule 18.01.2020