Разрешение нескольких интерфейсов различной формы в качестве возвращаемых типов TypeScript - PullRequest
2 голосов
/ 11 февраля 2020

У меня есть функция, которая принимает несколько параметров и генерирует объекты, которые будут переданы во внешний процесс. Поскольку у меня нет контроля над формами, которые необходимо в конечном итоге создать, я должен иметь возможность принимать некоторые изменяющиеся параметры для своей функции и собирать их в соответствующие объекты. Вот действительно базовый c пример, который показывает проблему, с которой я столкнулся:

interface T1A {
    type: 'type1';
    index: number;
}

interface T1B {
    type: 'type1';
    name: string;
}

interface T2A {
    type: 'type2';
    index: number;
}

interface T2B {
    type: 'type2';
    name: number;
}

function hello(type: 'type1' | 'type2'): T1A | T1B | T2A | T2B {
    return {type, index: 3}
}

Эта конкретная функция жалуется, что type1 не может быть назначен type2. Я немного прочитал об охране типов и выяснил, как сделать этот простой пример happpy:

function hello(type: 'type1' | 'type2'): T1A | T1B | T2A | T2B {
    if (type === 'type1') {
        return {type, index: 3}
    } else {
        return {type, index: 3}
    }
}

Однако я не понимаю, почему это необходимо. Проверка типа абсолютно ничего не делает с возможностями возвращаемого значения. Исходя из того факта, что мой параметр принимает только одну из двух явных строк, один оператор return гарантированно возвращает одну из T1A или T2A. В моем реальном случае использования у меня есть больше типов и параметров, но способ, которым я назначил их, всегда гарантированно возвращает по крайней мере один из указанных интерфейсов. Кажется, что объединение не является «этим ИЛИ этим» при работе с интерфейсами. Я пытаюсь разбить свой код для обработки каждого отдельного типа, но когда есть 8 различных возможностей, я получаю много дополнительных блоков if / else, которые кажутся бесполезными.

Я также пытался использовать типы type T1A = {...};

Не понимаю ли я что-то о профсоюзах, или это ошибка, или более простой способ борьбы с ней?

1 Ответ

3 голосов
/ 11 февраля 2020

TypeScript, как правило, не выполняет распространение соединений по свойствам. То есть, хотя каждое значение типа {foo: string | number} должно присваиваться типу {foo: string} | {foo: number}, компилятор не видит их как взаимно присваиваемые:

declare let unionProp: { foo: string | number };
const unionTop: { foo: string } | { foo: number } = unionProp; // error!

Кроме странных вещей, которые происходят при запуске изменяя свойства таких типов, компилятору вообще слишком много времени делать это, особенно когда у вас есть несколько свойств объединения. Соответствующий комментарий в microsoft / TypeScript # 12052 гласит:

. Этот тип эквивалентности имеет место только для типов с одним свойством и не является истинным в общем случае. Например, было бы неправильно считать { x: "foo" | "bar", y: string | number } эквивалентным { x: "foo", y: string } | { x: "bar", y: number }, поскольку первая форма допускает все четыре комбинации, тогда как вторая форма допускает только две указанные c единицы.

Так что это не ошибка как таковая, а ограничение: компилятор собирается потратить так много времени, играя в союзы, чтобы убедиться, что какой-то код допустим.


В TypeScript 3.5, добавлена ​​поддержка для выполнения вышеуказанных вычислений специально для случая дискриминируемых союзов . Если у вашего объединения есть свойства, которые можно использовать для различения guish между членами объединения (что означает одноэлементные типы, такие как строковые литералы , цифра c литералы , undefined или null) в хотя бы некотором члене объединения , тогда компилятор проверит ваш код так, как вы этого хотите.

Вот почему это неожиданно работает, если вы измените T1A | T2A | T1B | T2B на T1A | T2A, и может быть возможным ответом на ваш вопрос:

function hello(type: 'type1' | 'type2'): T1A | T2A | T1B | T2B {
    const x: T1A | T2A = { type, index: 3 }; // okay
    return x;
}

Последний является дискриминационным объединением: свойство type сообщает вам, какой член у вас есть. Но первое не так: свойство type может различать guish между T1A | T1B и T2A | T2B, но нет свойства для дальнейшего деления. Нет, простое отсутствие index или name в типе не считается дискриминантом, поскольку значение типа T1A может иметь свойство a name; типы в TypeScript не точные .

Приведенный выше код работает, потому что компилятор может проверить, что x имеет тип T1A | T2A, а затем может проверить, что T1A | T2A является подтипом из полного T1A | T2A | T1B | T2B. Поэтому, если вас устраивает двухэтапный процесс, это возможное решение для вас.


Если вы хотите, чтобы T1A | T2A | T1B | T2B был дискриминационным объединением, вам нужно изменить составные типы ' определения, чтобы действительно различать, по крайней мере, одним общим свойством. Например, вот так:

interface T1A {
    type: 'type1';
    index: number;
    name?: never;
}

interface T1B {
    type: 'type1';
    name: string;
    index?: never;
}

interface T2A {
    type: 'type2';
    index: number;
    name?: never;
}

interface T2B {
    type: 'type2';
    name: number;
    index?: never;
}

Добавляя эти необязательные свойства типа never, теперь вы можете использовать type, name, и index в качестве дискриминантов , Свойства name и index иногда будут undefined, одноэлементный тип, который поддерживается для различных типов guish. И тогда это работает:

function hello(type: 'type1' | 'type2'): T1A | T2A | T1B | T2B {
    return { type, index: 3 }; // okay
}

Так что это два варианта. Другой вариант, который всегда доступен в таких ситуациях, когда вы знаете, что что-то безопасно, а компилятор этого не делает, - это использование утверждения типа :

function hello2(type: 'type1' | 'type2') {
    return { type, index: 3 } as T1A | T2A | T1B | T2B
}

Хорошо, надеюсь что помогает; удачи!

Детская площадка ссылка на код

...