Как тип ввода может выбрать выход и поддерживать объединение? - PullRequest
0 голосов
/ 05 марта 2019

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

function _buffer<T>(size: number, iterable: AsyncIterable<T>): AsyncIterableIterator<T> {
  throw new Error('not important')

}

function* syncBuffer<T>(size: number, iterable: Iterable<T>): IterableIterator<T> {
  throw new Error('not important')
}

export function buffer<T>(
  size: number
): {
  (curriedIterable: AsyncIterable<T>): AsyncIterableIterator<T>
  (curriedIterable: Iterable<T>): IterableIterator<T>
  (curriedIterable: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>
}
export function buffer<T>(size: number, iterable: AsyncIterable<T>): AsyncIterableIterator<T>
export function buffer<T>(size: number, iterable: Iterable<T>): IterableIterator<T>
export function buffer<T>(size: number, iterable: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>
export function buffer<T>(size: number, iterable?: Iterable<T> | AsyncIterable<T>) {
  if (iterable === undefined) {
    return <R>(curriedIterable) => buffer<R>(size, curriedIterable)
  }
  if (iterable[Symbol.asyncIterator]) {
    return _buffer(size, iterable as AsyncIterable<T>)
  }

  return syncBuffer(size, iterable as Iterable<T>)
}


function run(a: AsyncIterable<any>) {
  return buffer(4, a)
}


function run(a: AsyncIterable<any> | Iterable<any>) {
  return buffer(4, a)
  return buffer(4)(a)
}

Однако я получаю следующую ошибку типа при компиляции.

Overload signature is not compatible with function implementation.ts(2394) 

// in reference to 
export function buffer<T>(size: number, iterable: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>

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

Ответы [ 2 ]

1 голос
/ 05 марта 2019

Это отдельный ответ, потому что он представляет другой подход. Одна из проблем, с которой люди иногда сталкиваются с сигнатурами перегрузки, заключается в том, что они не всегда ведут себя интуитивно с объединениями параметров . Вот глупый пример:

// call signatures
function foo(x: string): number;
function foo(x: number): string;
// implementation
function foo(x: string | number): number | string {
  return (typeof x === 'string') ? x.length : "a".repeat(x);
}

Функция foo() принимает string и возвращает number, или принимает number и возвращает string. И это работает как ожидалось:

const num: number = foo("string"); // okay
const str: string = foo(12345); // okay

Но люди ожидают, что вы можете передать ему что-то типа string | number и получить значение типа number | string. Иногда это ожидание происходит из-за смешения сигнатуры реализации с сигнатурой вызова, в других случаях просто кажется, что компилятор должен иметь возможность выбрать несколько перегрузок и выполнить их объединение. Но этого не происходит. Компилятор выбирает только одну сигнатуру перегрузки (во всяком случае, по крайней мере, для TS3.3. Раньше было невозможно вызвать объединение типов функций, но теперь вы можете ... ну, с предостережения . Возможно, со временем произойдет объединение перегрузки):

const oops: string | number = foo(Math.random() < 0.5 ? "string" : 12345); // error!

В моем другом ответе предлагается исправить это, добавив подпись вызова, специально соответствующую объединению. И это работает:

function foo(x: string): number;
function foo(x: number): string;
function foo(x: string | number): number | string; // added
function foo(x: string | number): number | string {
  return (typeof x === 'string') ? x.length : "a".repeat(x);
}

const num: number = foo("string"); // okay
const str: string = foo(12345); // okay
const oops: string | number = foo(Math.random() < 0.5 ? "string" : 12345); // okay

Но есть и другой способ. Вы можете использовать универсальные функции и условные типы , чтобы заменить три сигнатуры вызовов, например:

function foo<X extends string | number>(x: X): X extends string ? number : string;
function foo(x: string | number): number | string {
  return (typeof x === 'string') ? x.length : "a".repeat(x);
}

const num: number = foo("string"); // okay
const str: string = foo(12345); // okay
const oops: string | number = foo(Math.random() < 0.5 ? "string" : 12345); // okay

Почему это работает?

Итак, обобщенный тип X будет выводиться как любой подтип string | number на основе переданного параметра. Для foo("string"), X выводится как строковый литерал type "string". Для foo(12345), X выводится как числовой литерал тип 12345. И в вызове с Math.random(), X выводится как "string" | 12345. Таким образом, все вызовы должны быть успешными.

Что они возвращают? Вот где появляется условный тип. Тип X extends string ? number : string означает, что если X является подтипом string, то условный тип будет number. В противном случае условный тип будет string. Таким образом, для foo("string"), X extends string имеет значение true, а тип возвращаемого значения - number. Для foo(12345), X extends string имеет значение false, а тип возвращаемого значения - string. А что насчет этого типа объединения с Math.random()? Ну, поскольку условные типы распределяются по объединениям , в итоге получается number | string по желанию.

Вы можете или не хотите делать что-то похожее с вашей функцией:

type MaybeIterable<T> = AsyncIterable<T> | Iterable<T>;
type UnmaybeIterable<M extends MaybeIterable<any>> = M extends Iterable<infer T> ? Iterable<T> : M extends AsyncIterable<infer T> ? AsyncIterable<T> : never;
type CurriedBufferResult = {
  <M extends MaybeIterable<any>>(curriedIterable: M): UnmaybeIterable<M>
 };
export function buffer(
  size: number
): CurriedBufferResult;
export function buffer<M extends MaybeIterable<any>>(size: number, iterable: M): UnmaybeIterable<M>;
export function buffer(size: number, iterable?: MaybeIterable<any>): CurriedBufferResult | UnmaybeIterable<any>
{
  // impl here
  return null!;
}

Это то, что вы хотите? Не уверен.

1 голос
/ 05 марта 2019

Функция перегрузки в TypeScript разделяют сигнатуру функции на две стороны: одна представляет собой список вызовов сигнатур, видимых вызывающими функциями.Они также просто называются «сигнатурами перегрузки».Там может быть один или много из них.Сигнатуры вызовов не имеют тела.

Другая сторона - это сигнатура реализации , видимая реализацией функции , а не вызывающей стороной .Может быть только одна сигнатура реализации.Подпись реализации должна иметь тело.

Подписи вызовов должны предшествовать сигнатуре реализации.Сигнатура реализации должна быть «совместима с» сигнатурами вызовов (например, сигнатура реализации не может требовать параметр, не предоставленный какой-либо сигнатурой вызова), но это не одно и то же.


Ваша проблема:вы пытаетесь обработать подпись реализации как подпись вызова.

Исправление: добавьте дополнительную подпись вызова в конце списка.Он может совпадать с сигнатурой реализации:

// call signatures:
function foobar<T>(foo: AsyncIterable<T>): AsyncIterable<T>;
function foobar<T>(foo: Iterable<T>): Iterable<T>;
// add the following call signature
function foobar<T>(foo: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>;

// implementation signature:
function foobar<T>(foo: Iterable<T> | AsyncIterable<T>) {
  return foo
}

Надеюсь, это поможет.Удачи!


Обновлено для работы с новой формой:

type CurriedBufferResult<T> = {
  (curriedIterable: AsyncIterable<T>): AsyncIterableIterator<T>
  (curriedIterable: Iterable<T>): IterableIterator<T>
  (curriedIterable: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>
};
export function buffer<T>(
  size: number
): CurriedBufferResult<T>;
export function buffer<T>(size: number, iterable: AsyncIterable<T>): AsyncIterableIterator<T>
export function buffer<T>(size: number, iterable: Iterable<T>): IterableIterator<T>
export function buffer<T>(size: number, iterable: Iterable<T> | AsyncIterable<T>): Iterable<T> | AsyncIterable<T>
export function buffer<T>(size: number, iterable?: Iterable<T> | AsyncIterable<T>):
Iterable<T> | AsyncIterable<T> | CurriedBufferResult<T> 
{
  // impl here
  return null!;
}

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

Теперь вам нужно убедиться, что реализация (в // impl here) соответствует этой аннотации.Проблема, которую вы, вероятно, видели, заключается в том, что ваша реализация функции фактически не возвращала аннотированный выше тип, а выводимый тип реализации не соответствовал сигнатурам вызова.

Снова удачи.

...