Кто может объяснить этот странный вывод в TypeScript?

Вопрос или проблема

Кто может объяснить этот странный вывод в TypeScript?

Я уверен, что это ожидаемое поведение, но для меня это кажется неинтуитивным. Учитывая следующий фрагмент, почему item интерпретируется как { a: number }, а не как { b: number }? Я ожидал что-то вроде переопределения в Options & { activities: Array<{ b: number }>}.

введите описание изображения здесь

Я обнаружил, что замена типа параметра options на Omit<Options, 'activities'> & { activities: Array<{ b: number }> } решает проблему, но я хотел бы понять, почему TS работает таким образом, чтобы возникала необходимость в Omit<>.

Вот код, так что вы можете скопировать и вставить:

type Activity = { a: number };
type Options = { activities: Activity[] }

const fun = (options: Options & { activities: Array<{ b: number }> }) => {
    return options.activities.map(item => item.b)
}

Я ожидал переопределения свойства activities, чтобы оно получало тип { b: number }[].

На мой взгляд, это отсутствующая функция в системе типов TS:

type ActivityA = { a: number };
type ActivityB = { b: number };

const myArr0: ActivityA[] & ActivityB[] = [
    {a: 1, b: 2},
    {a: 1},         // ОШИБКА, B необходим
]

const item = myArr0[0];         //ОК, item это ActivityA & ActivityB
myArr0.map(item => item.b);     //ОШИБКА, x это ActivityA

Пересечение типов не является рекурсивным. Учитывая

type ActivityA = { a: number };
type ActivityB = { b: number };

Типы ActivityA[] & ActivityB[] и (ActivityA & ActivityB)[] явно не ведут себя одинаково.

type ActivityA = { a: number };
type ActivityB = { b: number };


const myArr0: ActivityA[] & ActivityB[] = [
    {a: 1, b: 2},
    {a: 1},         // ОШИБКА, B необходим
]

const myArr1: (ActivityA & ActivityB)[] = [
    {a: 1, b: 2},
    {a: 1},        // ОШИБКА, B необходим
]

const myArr2: ActivityB[] = myArr1;

myArr0.map(x => x.b); //ОШИБКА, x это ActivityA
myArr1.map(x => x.b); //ОК, x это ActivityA & ActivityB
myArr2.map(x => x.b); //ОК, x это ActivityB

Оператор & не является рекурсивным и ничего не переопределяет.

Цель TypeScript – улучшить код, а не ухудшить его.

https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#intersection-types

Ответ или решение

В TypeScript существует несколько особенностей работы с типами, которые могут показаться неинтуитивными. Давайте рассмотрим ваш пример и выясним, почему тип item выводится как { a: number }, а не { b: number }.

Объяснение проблемы

В вашем коде мы имеем два типа:

type Activity = { a: number };
type Options = { activities: Activity[] }

Затем вы объявляете функцию:

const fun = (options: Options & { activities: Array<{ b: number }> }) => {
    return options.activities.map(item => item.b);
}

Здесь вы используете оператор пересечения (&), чтобы объединить тип Options с другим объектом, который содержит activities в виде массива объектов с полем b. Однако, TypeScript не переопределяет свойство activities, а просто сочетает типы. Таким образом, по умолчанию при пересечении типов будет использована структура Activity[], а не { b: number }[].

Почему происходит такое поведение?

TypeScript обрабатывает пересечения типов таким образом, что он не "перезаписывает" существующие свойства. Когда вы говорите Options & { activities: Array<{ b: number }> }, TypeScript видит, что у Options уже есть свойство activities, и использует его тип, вместо того чтобы заменять его на новый тип. Это поведение следует из того факта, что пересечение типов в TypeScript не является рекурсивным обновлением, а скорее объединением нескольких типов в один, при этом конфликтующая информация (в данном случае типы свойств) сохраняется.

Для того чтобы корректно переопределить тип activities, вы можете воспользоваться Omit, как вы уже обнаружили:

const fun = (options: Omit<Options, 'activities'> & { activities: Array<{ b: number }> }) => {
    return options.activities.map(item => item.b);
}

Здесь мы исключаем исходное свойство activities из Options, после чего можем добавить новое определение, что позволяет TypeScript корректно интерпретировать новый тип.

Различие между типами

Вы упомянули следующие типы:

const myArr0: ActivityA[] & ActivityB[] = [
    {a: 1, b: 2},
    {a: 1},         // ERROR, B is necessary
]

const myArr1: (ActivityA & ActivityB)[] = [
    {a: 1, b: 2},
    {a: 1},        // ERROR, B is necessary
]

Каждый из этих типов ведет себя по-разному. ActivityA[] & ActivityB[] предполагает массив, в котором ожидаются элементы, соответствующие обоим типам одновременно, но не обязательно включающие их все. В то время как (ActivityA & ActivityB)[] ожидает, что каждый элемент массива будет одновременно соответствовать обоим типам, включая как a, так и b.

Заключение

Таким образом, ответ на ваш вопрос заключается в том, что TypeScript не заменяет исходное свойство в пересечении типов, а комбинирует их, что может привести к путанице. Если вы хотите переопределить тип свойства, используйте Omit, чтобы удалить его из исходного типа перед добавлением нового определения.

Если вы хотите видеть в TypeScript более "интуитивное" поведение, возможно, вы можете рассмотреть использование других конструкций, например, расширение типов или использование объединений вместо пересечений в подходящих случаях.

Оцените материал
Добавить комментарий

Капча загружается...