Заранее извиняюсь, если это дубликат, но мой поиск не дал ничего, что полностью соответствовало бы моей проблеме.
Во-первых, желаемое поведение состоит в том, чтобы иметь метод класса с двумя параметрами, второй необязательный, где тип второго параметра зависит от типа первого параметра. Если первый параметр имеет тип 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. 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, поэтому могут быть и другие варианты, но я могу придумать две альтернативы:
Использование аргумента rest с различным типом кортежа
Использование размеченного союза, поэтому всегда есть только один параметр
В тех местах, где перегрузки функций казались мне слишком громоздкими, я склонялся к размеченным объединениям, но идея с кортежами была довольно милой, поэтому я решил включить ее.
Вместо 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});
@TJCrowder Не беспокойтесь, я на самом деле сам сделал публикацию и запуск, поскольку опубликовал это прямо перед тем, как убежать на несколько часов :-). Взгляните на ваш ответ сейчас
@TJCrowder аааа... Понятно. Очень интересно, и немного неожиданно, и трудно получить из документов по перегрузкам функций, и, насколько я могу судить, документация по классам конкретно не касается перегрузок методов. Тем не менее, я думаю, что понимаю это сейчас, и первое решение (просто добавить перегрузки непосредственно в метод класса [что я даже не осознавал, что вы можете это сделать]) имеет наибольший смысл и кажется лучшим решением. Это именно то исправление, которое мне было нужно. Спасибо!
@TJCrowder Что касается второй части вопроса, есть ли у вас какие-либо лучшие предложения о том, как этого добиться? то есть в некоторых случаях требуется необязательный параметр, но не во всех. Это может стать подробным, если имеется много типов сообщений с разными типами полезных данных, и вам также придется продолжать поддерживать перегрузку для каждого типа сообщений, которые также не требуют полезных данных. Возможно, это так же хорошо, как и возможно, но открыто для других подходов (хотя это работает как шарм в моей текущей настройке).
@no_stack_dub_sack - Извините, я просто пропустил вторую часть, не так ли? Я добавил в конец ответа. Я могу думать о двух альтернативах. Надеюсь это поможет!
@TJCrowder Хм ... второй вариант с типами объединения кажется слишком многословным и сложным в обслуживании по-другому, и он определенно предпочел бы подход с перегрузкой по сравнению с этим. Однако первый вариант кажется наименее обслуживаемым из всех, поскольку единственное, что нужно обновить при добавлении нового типа сообщения, — это MessageParams<T>
. Тем не менее, интеллект, который VS Code предоставляет при таком подходе, не так хорош, поскольку вы просто получаете arg0
и arg1
вместо именованных аргументов. Это может быть приемлемым компромиссом, тем более что сигнатура вызова при таком подходе вообще не меняется.
@TJCrowder Я также нашел способ немного улучшить исходное решение для перегрузки. По сути, вы поддерживаете один дополнительный тип, представляющий собой белый список (объединение) типов сообщений, для которых не требуются полезные данные. Таким образом, вам нужно иметь только две перегрузки, синтаксис перегрузки останется кратким, и если разработчик не обновит один из двух типов, код не скомпилируется. Это требует немного больше усилий, чем подход с кортежем, но, по крайней мере, вам никогда не придется прикасаться к перегрузке, и вы по-прежнему получаете хороший интеллект в месте вызова: короткая ссылка.at/ahtyM
@TJCrowder Спасибо за обновленный ответ!
Извиняюсь за пост-и-бег (я не фанат этого!), но меня отозвали. Я вернусь позже, чтобы ответить на все, что появится в комментариях. Простите за опоздание.