Перегрузки метода класса TypeScript не ведут себя так же, как перегрузки функций

Заранее извиняюсь, если это дубликат, но мой поиск не дал ничего, что полностью соответствовало бы моей проблеме.

Во-первых, желаемое поведение состоит в том, чтобы иметь метод класса с двумя параметрами, второй необязательный, где тип второго параметра зависит от типа первого параметра. Если первый параметр имеет тип A, второй параметр всегда должен быть обязательным и должен быть типа X, если первый параметр имеет тип B, второй параметр следует опустить.

Я добился чего-то подобного с перегрузкой функций:

// types
enum MessageType { FOO, BAR, BAZ }

type MessagePayload<T extends MessageType> = T extends MessageType.FOO 
    ? string
    : T extends MessageType.BAR
    ? number
    : never;

// overloads
function sendMessage<T extends MessageType.BAZ>(action: T): void

function sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void

// implementation
function sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
  // do something
}

// tests
sendMessage(MessageType.FOO, "10") // no error - as expected
sendMessage(MessageType.FOO, 10)   // error - as expected, payload is not string
sendMessage(MessageType.FOO)       // error - as expected, payload must be string
sendMessage(MessageType.BAZ);      // no error - as expected - since MessageType is BAZ

Однако применение одних и тех же конструкций к методу класса не дает таких же результатов. Этот фрагмент является продолжением первого и использует те же типы:

// interface
interface ISomeClient {
  sendMessage<T extends MessageType.BAZ>(action: T): void

  sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void
}

// implementation
class SomeClient implements ISomeClient {
  sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
    // do something
  }
}

// tests
const client = new SomeClient();

client.sendMessage(MessageType.FOO, "10"); // no error - as expected
client.sendMessage(MessageType.FOO, 10);   // error, payload is not string
client.sendMessage(MessageType.FOO)        // no error??? different behavior than function example
client.sendMessage(MessageType.BAZ);       // this part works fine

Вот более полный пример на ТС игровая площадка.

Итак, я предполагаю, что это двухчастный:

  1. почему это не работает для примера класса?
  2. есть ли лучший способ добиться этого, который будет работать как для классов, так и для функций и не требует поддержки перегрузки для захвата типов, которым не требуется полезная нагрузка? Я использовал перечисление и условный тип здесь, чтобы ограничить второй параметр, чтобы он соответствовал тому, что ожидается с учетом первого параметра. Я играл с другой путь, используя ключ для ввода карты, но это кажется хакерским, по-прежнему требует перегрузок и страдает от той же проблемы для классов и функций.

Спасибо.

Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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
0
20
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

1. why is this not working for the class example?

Я думаю, проблема в том, что сигнатура в class не рассматривается как просто сигнатура реализации, как сигнатура третьей автономной функции, потому что перегрузки объявляются отдельно. Таким образом, class — это увеличение, которые добавляют третью общедоступную подпись, в отличие от перегрузок функций, где третья подпись не является общедоступной, это просто подпись реализации.

Вы можете исправить это, не помещая перегрузки (просто) в объявление интерфейса. Либо не используйте интерфейс:

class SomeClient {
  sendMessage<T extends MessageType.QAT | MessageType.QAZ>(action: T): void;
  sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void;
  sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
    // do something
  }
}

Пример детской площадки

...или использовать интерфейс, но также повторять перегрузки в конструкции class, чтобы TypeScript знал, что третья является сигнатурой реализации:

interface ISomeClient {
  sendMessage<T extends MessageType.QAT | MessageType.QAZ>(action: T): void
  sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void
}

class SomeClient implements ISomeClient {
  sendMessage<T extends MessageType.QAT | MessageType.QAZ>(action: T): void
  sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void
  sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
    // do something
  }
}

Пример детской площадки

Это повторяется, но я не уверен, что есть способ обойти это, кроме как назначить SomeClient.prototype постфактум.

2. is there some better way to achieve this...

Мне обычно нравятся перегрузки функций для этого, но это правда, что они не работают для всего, и если у вас их много, это быстро станет громоздким.

Я должен отметить, что я все еще нахожусь на уровне подмастерья с TypeScript, поэтому могут быть и другие варианты, но я могу придумать две альтернативы:

  1. Использование аргумента rest с различным типом кортежа

  2. Использование размеченного союза, поэтому всегда есть только один параметр

В тех местах, где перегрузки функций казались мне слишком громоздкими, я склонялся к размеченным объединениям, но идея с кортежами была довольно милой, поэтому я решил включить ее.

Отдых + Кортеж

Вместо MessagePayload<T> у вас есть MessageParams<T> определение кортежа на основе T:

type MessageParams<T extends MessageType> = T extends MessageType.FOO 
    ? [T, string]
    : T extends MessageType.BAR
    ? [T, number]
    : T extends MessageType.BAZ
    ? [T, User]
    : [T];

(Если вам нужно MessagePayload<T> по другим причинам, вы можете получить его из приведенного выше: type MessagePayload2<T extends MessageType> = MessageParams<T>[1];.)

Затем метод использует это как тип остаточного параметра:

class SomeClient {
  sendMessage<T extends MessageType>(...args: MessageParams<T>) {
    const action = args[0];
    // do something
  }
}

Пример детской площадки

Однако опыт разработчика очень похож на перегрузку.

Дискриминированный союз

Этот последний вариант является большим изменением: у вас вообще нет отдельных параметров, только один тип объекта, который представляет собой размеченное объединение:

type FOOMessage = {action: MessageType.FOO; payload: string;};
type BARMessage = {action: MessageType.BAR; payload: number;};
type BAZMessage = {action: MessageType.BAZ; payload: User;};
type OtherMessage = {action: Exclude<MessageType, MessageType.FOO | MessageType.BAR | MessageType.BAZ>;};
// `OtherMessage` is the catch-all for all message types other than the
// ones with their own interface, note the use of `Exclude`

type Message = FOOMessage | BARMessage | BAZMessage | OtherMessage;

// ...

class SomeClient {
  sendMessage(message: Message) {
    const action = message.action;
    // do something
  }
}

Вызовы к нему меняются на передачу объекта:

// tests
client.sendMessage({action: MessageType.FOO, payload: "string"});
client.sendMessage({action: MessageType.FOO}); // Error as desired
client.sendMessage({action: MessageType.QAT});

Пример детской площадки

Извиняюсь за пост-и-бег (я не фанат этого!), но меня отозвали. Я вернусь позже, чтобы ответить на все, что появится в комментариях. Простите за опоздание.

T.J. Crowder 06.04.2022 23:39

@TJCrowder Не беспокойтесь, я на самом деле сам сделал публикацию и запуск, поскольку опубликовал это прямо перед тем, как убежать на несколько часов :-). Взгляните на ваш ответ сейчас

no_stack_dub_sack 07.04.2022 03:07

@TJCrowder аааа... Понятно. Очень интересно, и немного неожиданно, и трудно получить из документов по перегрузкам функций, и, насколько я могу судить, документация по классам конкретно не касается перегрузок методов. Тем не менее, я думаю, что понимаю это сейчас, и первое решение (просто добавить перегрузки непосредственно в метод класса [что я даже не осознавал, что вы можете это сделать]) имеет наибольший смысл и кажется лучшим решением. Это именно то исправление, которое мне было нужно. Спасибо!

no_stack_dub_sack 07.04.2022 03:16

@TJCrowder Что касается второй части вопроса, есть ли у вас какие-либо лучшие предложения о том, как этого добиться? то есть в некоторых случаях требуется необязательный параметр, но не во всех. Это может стать подробным, если имеется много типов сообщений с разными типами полезных данных, и вам также придется продолжать поддерживать перегрузку для каждого типа сообщений, которые также не требуют полезных данных. Возможно, это так же хорошо, как и возможно, но открыто для других подходов (хотя это работает как шарм в моей текущей настройке).

no_stack_dub_sack 07.04.2022 03:21

@no_stack_dub_sack - Извините, я просто пропустил вторую часть, не так ли? Я добавил в конец ответа. Я могу думать о двух альтернативах. Надеюсь это поможет!

T.J. Crowder 07.04.2022 10:08

@TJCrowder Хм ... второй вариант с типами объединения кажется слишком многословным и сложным в обслуживании по-другому, и он определенно предпочел бы подход с перегрузкой по сравнению с этим. Однако первый вариант кажется наименее обслуживаемым из всех, поскольку единственное, что нужно обновить при добавлении нового типа сообщения, — это MessageParams<T>. Тем не менее, интеллект, который VS Code предоставляет при таком подходе, не так хорош, поскольку вы просто получаете arg0 и arg1 вместо именованных аргументов. Это может быть приемлемым компромиссом, тем более что сигнатура вызова при таком подходе вообще не меняется.

no_stack_dub_sack 07.04.2022 17:10

@TJCrowder Я также нашел способ немного улучшить исходное решение для перегрузки. По сути, вы поддерживаете один дополнительный тип, представляющий собой белый список (объединение) типов сообщений, для которых не требуются полезные данные. Таким образом, вам нужно иметь только две перегрузки, синтаксис перегрузки останется кратким, и если разработчик не обновит один из двух типов, код не скомпилируется. Это требует немного больше усилий, чем подход с кортежем, но, по крайней мере, вам никогда не придется прикасаться к перегрузке, и вы по-прежнему получаете хороший интеллект в месте вызова: короткая ссылка.at/ahtyM

no_stack_dub_sack 07.04.2022 17:14

@TJCrowder Спасибо за обновленный ответ!

no_stack_dub_sack 07.04.2022 17:31

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

Похожие вопросы

Передача vscode во внешние функции
Firebase: Почему мой код использует только первый документ в моей коллекции?
Реагировать на ошибку машинописного текста - элемент неявно имеет любой тип, потому что выражение строки типа не может использоваться для индексирования типа {}
Как получить доступ к вложенным необязательным индексам из интерфейса
Почему машинописный текст не может контекстуально вывести эти типы промежуточного программного обеспечения
Передать конструктор класса как функцию в другом классе
React & clsx: добавьте имя класса, если текущий элемент в сопоставленном массиве является первым из нескольких элементов
Свойство «MathFun» отсутствует в типе «(x?: число, y?: число) => число», но требуется в типе «Func»
Получение строки типа не может быть назначено строке типа для компонента TS в сборнике рассказов
Как ввести useState для файлов?