Два, казалось бы, одинаковых типа ведут себя по-разному?

Я пытался создать тип утилиты 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 сосуществовать. Чтобы они не сосуществовали, в каждом из типов объединения должно быть свойство с постоянным типом. (например, x: true и x: false)

HairyHandKerchief23 13.08.2024 13:14

Если вы хотите добиться этого, я нашел способ сделать это

HairyHandKerchief23 13.08.2024 13:22

Спасибо. Есть идеи, почему эта версия работает? Похоже, для меня это снова должно быть эквивалентно. А также относительно союза, позволяющего сосуществовать w и z, почему это работает? Я читал | как «или», но это явно не тот случай. Что могло бы произвести то, что я ожидал?

Matthew 13.08.2024 14:13

• TS использует определения объединения/пересечения из теории множеств, но ссылается на наборы значений, соответствующие типу, а не на наборы определений свойств. Типы контравариантны по своим ключам, поэтому объединения/пересечения меняются местами. См. этот вопрос . Поэтому вы используете нетрадиционные имена, когда ссылаетесь на объединения и пересечения в TypeScript; пожалуйста отредактируйте, чтобы либо использовать другую терминологию, либо четко указать, что вы пересекаете и объединяете (например, IntersectProps). Иначе это просто отвлечение от вашего вопроса о том, как ведет себя ТС.

jcalz 13.08.2024 14:25

• Как только вы это сделаете, ответ будет заключаться в том, что 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; если нет, то что мне не хватает?

jcalz 13.08.2024 14:32

Спасибо! Да, это касается большей части проблем, хотя я хотел бы спросить, что именно считается «нарушением» этого in keyof в достаточной степени? Как это определяется? Кроме того, из вашего объяснения я понял, почему A | B производит то, что делает, знаете, как бы мне произвести то, что я ожидал?

Matthew 13.08.2024 15:00

• Я не знаю, как это «определено», пока не пройдусь по реализации компилятора, чего я не склонен делать. С эвристической точки зрения, если вы напишете in X где определение X не является буквально keyof Y, то оно в основном нарушено. Вы можете прервать с помощью in (PropertyKey & keyof Y) или in T, где T определен в другом месте, и т. д. Этого достаточно?

jcalz 13.08.2024 15:07

• «Знаете ли вы, как мне добиться того, чего я ожидал»? Вам следует задать всего один вопрос, а не два . Я предлагаю вам удалить эту часть, а затем посмотреть вопрос/ответ, ссылку на который я дал выше, чтобы понять, почему объединения типов в конечном итоге соответствуют пересечениям свойств, но допускают действия других членов.

jcalz 13.08.2024 15:09

Спасибо, достаточно, буду рад принять ответ :)

Matthew 13.08.2024 15:10
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
9
62
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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' ответ правильный

Matthew 13.08.2024 16:58
Ответ принят как подходящий

Определение

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 частью своей документации» - правда? Ой.

Bergi 13.08.2024 17:10

Другие вопросы по теме