Союз Typescript в необязательном универсальном не работает

У меня есть тип, который принимает необязательный универсальный. Если предоставлен общий G, должно быть включено новое свойство типа G. Однако у меня возникла проблема, когда я делаю это в функции:

interface Message {
  id: string;
  message: string;
}

interface MessageWithContext<T> extends Message {
  context: T;
};

// Optional generic. If generic provided,
// context must also provided.
type NotificationMessage<T = void> = T extends void
  ? Message
  : MessageWithContext<T>;

interface Context1 {
  status: 'GOOD';
}

interface Context2 {
  status: 'BAD';
  error: string;
}

type AllContext = Context1 | Context2;

// DOES NOT WORK. See error below.
function toMessage(context: AllContext): NotificationMessage<AllContext> {
  return {
    id: '123',
    message: 'hello world',
    context: context
  };
}

Ссылка на игровую площадку.

Ошибка, которую я получаю:

Type '{ id: string; message: string; context: AllContext; }' is not assignable to type 'MessageWithContext<Context1> | MessageWithContext<Context2>'.
  Type '{ id: string; message: string; context: AllContext; }' is not assignable to type 'MessageWithContext<Context2>'.
    Types of property 'context' are incompatible.
      Type 'AllContext' is not assignable to type 'Context2'.
        Property 'error' is missing in type 'Context1' but required in type 'Context2'.(2322)

Ваша ссылка на игровую площадку и код здесь не приводят к ошибке, которую вы показываете. Вы забыли собственность id? Полезно иметь истинный минимальный воспроизводимый пример, чтобы люди могли немедленно приступить к решению проблемы, не создавая ее заново.

jcalz 18.03.2022 15:51

Соответствует ли этот подход вашим потребностям? Если вы не собираетесь использовать NotificationMessage<T> как дистрибутив над союзами в T, вам не нужен распределительный условный тип, и все начнет работать, когда вы измените его на обычный условный тип.

jcalz 18.03.2022 15:55

(Пожалуйста, отредактируйте код открытого текста, а не только ссылку на игровую площадку)

jcalz 18.03.2022 15:56

Спасибо @jcalz! Я исправил и ссылку, и открытый текст. И ДА, это исправляет это! Благодарю вас! Почему размещение квадратных скобок делает его недистрибутивным? Приведенная ссылка об этом не говорит.

azizj 18.03.2022 16:02
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
4
48
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это определение NotificationMessage:

type NotificationMessage<T = void> = T extends void
  ? Message
  : MessageWithContext<T>;

является распределительный условный тип, потому что тип T, который вы проверяете, является параметром универсального типа. Распределительные условные типы распределять — операция типа над союзы в соответствующем аргументе типа, так что NotificationMessage<A | B | C> будет оцениваться как NotificationMessage<A> | NotificationMessage<B> | NotificationMessage<C>:

type M = NotificationMessage<AllContext>
// type M = MessageWithContext<Context1> | 
//   MessageWithContext<Context2>

Это означает, что NotificationMessage<AllContext> сам по себе является типом объединения. И хотя значение { id: '123', message: 'hello world', context: context }мог можно считать присваиваемым такому типу объединения, компилятор не предпринимает никаких попыток это проверить. Это только пытается сделать это, если целевой тип — дискриминированный союз, а NotificationMessage<AllContext> не один (у него нет дискриминантного свойства... нет, подсвойства не учитываются, см. мс/ТС#18758). Значит есть ошибка.

Если вы действительно хотите, чтобы NotificationMessage<AllContext> был типом союза, мы могли бы посмотреть, как исправить toMessage(), чтобы он работал. Но вы, кажется, на самом деле не пытаетесь это сделать. Вероятно, вы были бы счастливы, если бы NotificationMessage<AllContext> был одним типом объекта со свойством context типа union.

Это означает, что вы не хотите, чтобы NotificationMessage<T> распространялось в T. Самый простой способ «отключить» дистрибутивность — заключить каждую сторону условия T extends U в квадратные скобки, чтобы получился одноэлементный кортежи:

type NotificationMessage<T = void> = [T] extends [void]
  ? Message
  : MessageWithContext<T>;

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

type M = NotificationMessage<AllContext>
// type M = MessageWithContext<AllContext>

И теперь все просто работает:

function toMessage(context: AllContext): NotificationMessage<AllContext> {
  return {
    id: '123',
    message: 'hello world',
    context: context
  }; // okay
}

Ссылка на код для игровой площадки

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