Вопрос или проблема
Кто может объяснить этот странный вывод в 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 более "интуитивное" поведение, возможно, вы можете рассмотреть использование других конструкций, например, расширение типов или использование объединений вместо пересечений в подходящих случаях.