Недавно в RxJS Primitives я столкнулся с ситуацией, когда один из методов — concat изначально был рассчитан на получение в качестве аргумента списка строк, а в методе использовались остальные (...args) параметры, имитирующие сигнатуру и передав их в String .prototype.concat

Я создал Проект StackBlitz с кодом для каждого
шага, которому можно следовать.

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

export function concat(...args: string[]): MonoTypeOperatorFunction<string> {
  return (source: Observable<string>) => source.pipe(map(value => value.concat(...args)))
}
fromString('Testing').pipe(concat([' one', ' two'])).subscribe(console.log)
Argument of type 'string[]' is not assignable to parameter of type 'string'.

Из-за того, как TypeScript обрабатывает остальные параметры, он ожидает список параметров в виде одной строки, которая превращается в аргументы, подобные массиву.

Чтобы обойти это, мы можем использовать TypeScript function перегрузка.

Как перегрузить функции с остальными параметрами

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

function concat(...args: string[]): MonoTypeOperatorFunction<string>;
function concat(args: string[]): MonoTypeOperatorFunction<string>;
function concat(...args: string[]): MonoTypeOperatorFunction<string> {
  return (source: Observable<string>) => source.pipe(map(value => value.concat(...args)))
}
export { concat }

Это привело к ошибке во второй реализации:

function concat(args: string[]): MonoTypeOperatorFunction<string> (+1 overload)
This overload signature is not compatible with its implementation signature.

Похоже, что TypeScript не может преобразовать тип остатка Array в значение, подобное массиву arguments, чтобы обойти это, нам нужно использовать значение any в последней реализации:

function concat(...args: string[]): MonoTypeOperatorFunction<string>;
function concat(args: string[]): MonoTypeOperatorFunction<string>;
function concat(...args: any): MonoTypeOperatorFunction<string> {
  return (source: Observable<string>) => source.pipe(map(value => value.concat(...args)))
}
export { concat }

Итак, теперь компилятор TypeScript перестал жаловаться, и мы можем протестировать его, используя оба поддерживаемых типа аргументов:

fromString('Testing').pipe(concat(' one', ' two')).subscribe(console.log) // Testing one two
fromString('Testing').pipe(concat([' one', ' two'])).subscribe(console.log) // Testing one, two

Однако, если вы посмотрите на вывод результата, вы заметите ошибку в нашей реализации Array, где в тексте появляется запятая — проблема в том, что теперь реализация обрабатывает массив как первый аргумент в списке arguments — и изменение последнего метода на args:any удалит разрушение нашего оставшегося параметра.

Чтобы решить эту проблему, нам нужно снова использовать тип any, чтобы деструктурировать наши аргументы и проверить, является ли первый из них массивом, если это так, мы используем его в качестве наших деструктурированных аргументов в методе String.prototype.concat, но если это строка, мы передаем все аргументы с использованием деструктуризации:

function concat(...args: string[]): MonoTypeOperatorFunction<string>;
function concat(args: string[]): MonoTypeOperatorFunction<string>;
function concat(...args: any): MonoTypeOperatorFunction<string> {
  const inArgs: any[] = [...args];
  if (inArgs[0] instanceof Array) {
    return (source: Observable<string>) => source.pipe(map(value => value.concat(...inArgs[0])))
  }
  return (source: Observable<string>) => source.pipe(map(value => value.concat(...inArgs)))
}
export { concat }

Теперь реализация работает так, как задумано — наш метод может принимать один или несколько строковых аргументов или массив строк в качестве первого аргумента — и их невозможно перепутать — если вы попытаетесь добавить к аргументам второй массив, компилятор TypeScript будет искать однострочные аргументы.