Я пытался создать тип утилиты IntersectProps, чтобы найти тип, который имеет пересечение наборов свойств, например, если
type A = {
x?: string;
y?: string;
z?: string;
};
type B = {
x?: string;
y?: string;
w?: string;
};
тогда IntersectProps из двух будет:
{
x?: string;
y?: string;
}
В этом вопросе я нашел способ сделать это для двух типов Typescript, как создать тип с общими свойствами двух типов?
используя тип
type Common<A, B> = { [P in keyof A & keyof B]: A[P] | B[P]; }
Однако это работает только для поиска пересечения двух типов, требуя нескольких вложенных приложений Common
, если их больше.
Я знаю, что выполнение keyof U
, где U — тип объединения, приводит к объединению ключей набора, который я ищу.
То есть keyof (A | B)
— это тип 'x' | 'y'
Поэтому, чтобы создать тот тип, который я хочу, я могу сделать:
type AisectpropsB = {
[K in keyof (A | B)]: (A | B)[K];
};
Обратите внимание, что это работает: AisectpropsB
— это тип `
{
x?: string;
y?: string;
}
Теперь я подумал, что смогу обобщить это следующим образом:
type IntersectProps<U> = {
[K in keyof U]: U[K];
};
такой, что IntersectProps<A | B>
эквивалентно AisectpropsB
. Тогда, если бы у меня было больше типов для пересечения, я бы легко это сделал IntersectProps<A|B|C|D|E|...|Z>
.
Однако вот моя проблема: по какой-то причине IntersectProps<A | B>
НЕ эквивалентно AisectpropsB
.
IntersectProps<A | B>
вместо этого разрешается в IntersectProps<A> | IntersectProps<B>
, что эквивалентно A | B
, что явно не то же самое, что AisectpropsB
.
(Обновлено: благодаря HairyHandKerchief23 в комментариях это работает, если вы определяете IntersectProps как
type IntersectProps<U, T extends keyof U = keyof U> = {
[K in T]: U[K];
};
но это все еще не объясняет, почему мое первоначальное определение не работает, это снова похоже на то, что оно должно быть эквивалентно моему первоначальному IntersectProps
определению)
Как такое может быть?
Здесь у меня также есть еще одна путаница, поскольку оказывается, что
A | B
далее сокращается до
{
x?: string;
y?: string;
z?: string;
w?: string;
};
чего я тоже не ожидал. Я ожидал, что A & B
выдаст такой результат, но я ожидал, что A | B
выдаст следующий тип, не позволяя сосуществовать «w» и «z»:
{
x?: string;
y?: string;
z?: string;
} | {
x?: string;
y?: string;
w?: string;
} ;
Что здесь происходит?
Если вы хотите добиться этого, я нашел способ сделать это
Спасибо. Есть идеи, почему эта версия работает? Похоже, для меня это снова должно быть эквивалентно. А также относительно союза, позволяющего сосуществовать w и z, почему это работает? Я читал |
как «или», но это явно не тот случай. Что могло бы произвести то, что я ожидал?
• TS использует определения объединения/пересечения из теории множеств, но ссылается на наборы значений, соответствующие типу, а не на наборы определений свойств. Типы контравариантны по своим ключам, поэтому объединения/пересечения меняются местами. См. этот вопрос . Поэтому вы используете нетрадиционные имена, когда ссылаетесь на объединения и пересечения в TypeScript; пожалуйста отредактируйте, чтобы либо использовать другую терминологию, либо четко указать, что вы пересекаете и объединяете (например, IntersectProps
). Иначе это просто отвлечение от вашего вопроса о том, как ведет себя ТС.
• Как только вы это сделаете, ответ будет заключаться в том, что type IsectProps<U> = { [K in keyof U]: U[K] }
— это гомоморфный отображаемый тип, как это реализовано в ms/TS#12447, и они распределяются по объединениям, поэтому IsectProps<A | B>
будет эквивалентно IsectProps<A> | IsectProps<B>
. Если вы хотите отключить дистрибутивность по объединениям, вам нужно сделать отображаемый тип негомоморфным, достаточно нарушив это in keyof
(как это делает альтернативное определение с K in T
). Это полностью решает вопрос? Если да, то я напишу a; если нет, то что мне не хватает?
Спасибо! Да, это касается большей части проблем, хотя я хотел бы спросить, что именно считается «нарушением» этого in keyof
в достаточной степени? Как это определяется? Кроме того, из вашего объяснения я понял, почему A | B производит то, что делает, знаете, как бы мне произвести то, что я ожидал?
• Я не знаю, как это «определено», пока не пройдусь по реализации компилятора, чего я не склонен делать. С эвристической точки зрения, если вы напишете in X
где определение X
не является буквально keyof Y
, то оно в основном нарушено. Вы можете прервать с помощью in (PropertyKey & keyof Y)
или in T
, где T
определен в другом месте, и т. д. Этого достаточно?
• «Знаете ли вы, как мне добиться того, чего я ожидал»? Вам следует задать всего один вопрос, а не два . Я предлагаю вам удалить эту часть, а затем посмотреть вопрос/ответ, ссылку на который я дал выше, чтобы понять, почему объединения типов в конечном итоге соответствуют пересечениям свойств, но допускают действия других членов.
Спасибо, достаточно, буду рад принять ответ :)
AisectpropsB — это не то же самое, что IntersectProps, поскольку AisectpropsB учитывает все свойства из объединения A и B, тогда как IntersectProps (используемый с типами пересечений) будет включать только общие свойства и их пересекающиеся типы.
AisectpropsB отражает объединение свойств A и B, при этом тип является объединением типов каждого свойства.
IntersectProps (где U — это тип, такой как A и B) отражает свойства, общие для A и B, с их типами из пересечения этих свойств.
Вместо этого вы можете использовать следующее:
type IntersectionOfTwo<T1, T2> = {
[K in keyof (T1 | T2)]: (T1 | T2)[K];
};
type IntersectionOfThree<T1, T2, T3> =
IntersectionOfTwo<IntersectionOfTwo<T1, T2>, T3>;
type IntersectionOfFour<T1, T2, T3, T4> =
IntersectionOfTwo<IntersectionOfThree<T1, T2, T3>, T4>;
// ...
type x = IntersectionOfTwo<A, B>;// { x?: string; y?: string; }
type y = IntersectionOfThree<A, B, { x?: string }>; // { x?: string; }
Это не решает вопрос, поскольку я использовал IntersectProps с типами объединения, например. IntersectProps<A|B>
не типы пересечений. jcalz' ответ правильный
Определение
type IntersectProps<U> = {
[K in keyof U]: U[K];
};
— это гомоморфный отображаемый тип , как это реализовано в microsoft/TypeScript#12447 (где вместо этого он называется «изоморфным»). См. Что означает «гомоморфный отображаемый тип»? . Отличительной чертой таких типов является in keyof
. Если у вас есть гомоморфный отображаемый тип, он, среди прочего, распределяет свою операцию по объединениям, так что IntersectProps<X | Y | Z>
эквивалентно IntersectProps<X> | IntersectProps<Y> | IntersectProps<Z>
.
Это явно не то, что вы хотите сделать, поэтому вам нужно отключить дистрибутив. Есть различные способы сделать это, каждый из которых предполагает достаточное «нарушение» in keyof
, например, написание in ({} & keyof U)
или in T
, где T
определено в другом месте.
Многие способы отключения дистрибутивности, к сожалению, также отключают ту часть гомоморфных отображаемых типов, которая вам нравится: копирование модификатора необязательности из входных в выходные свойства. Тот, который вы упомянули
type IntersectProps<U, T extends keyof U = keyof U> = {
[K in T]: U[K];
};
хорошо справляется с отключением дистрибутивности, сохраняя при этом копирование опционального свойства, потому что наличие T
ограничения до keyof U
имеет такой эффект (в основном, чтобы позволить типу утилиты Pick<T, K> работать по желанию.
Многие из этих вещей не так-то легко найти в документации (команда TS считает, что проблемы GitHub являются частью их документации, но это не облегчает поиск), поэтому я не могу указать простого места, которое бы определяло как именно включать и выключать различные функции отображаемых типов. По большей части это эвристические знания, полученные в результате частого использования языка.
«Команда TS считает проблемы GitHub частью своей документации» - правда? Ой.
В последнем фрагменте кода этот союз позволяет
w
иz
сосуществовать. Чтобы они не сосуществовали, в каждом из типов объединения должно быть свойство с постоянным типом. (например,x: true
иx: false
)