Странное поведение вывода типа в машинописном тексте - PullRequest
1 голос
/ 28 апреля 2020

Предположим, этот код:

// base class:
class Pin<Output, Input=Output> {
  to<Out>(pin: Pin<Out, Output>): Pin<Out, Output> {
    return pin;
  }

  from<In>(pin: Pin<Input, In>): Pin<Input, In> {
    return pin;
  }
}

//some type aliasing for convenience:
type SyncFunc<I, O> = (i: I) => O;
type Resolve<T> = SyncFunc<T, void>;
type AsyncFunc<I, O> = (i: I, cb: Resolve<O>) => void;
type Func<I, O> = SyncFunc<I, O> | AsyncFunc<I, O>;

function src<Type>(): Pin<Type> { return new Pin<Type>(); }

function map<I, O>(m: SyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: AsyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: SyncFunc<I, O> | AsyncFunc<I, O>): Pin<O, I> {
  return new Pin<O, I>();
}

При этой настройке следующий код будет иметь ошибки:

src<number>().to(map((i, c: Resolve<number>) => c(i * 2))).to(map(x => x + 1));

Поскольку тип i не выводится должным образом.

Я могу изменить расположение перегруженных подписей следующим образом:

function map<I, O>(m: AsyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: SyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: SyncFunc<I, O> | AsyncFunc<I, O>): Pin<O, I> {
  return new Pin<O, I>();
}

Это устраняет проблему с предыдущим примером, но вызывает проблему с этим:

src<number>().to(map(i => i * 2)).to(map(x => x + 1));

Поскольку функция переданный в map() теперь предполагается, что он имеет тип AsyncFunc, возвращаемый тип не разрешен, и поэтому предполагается, что x имеет тип unknown, что приводит к другой ошибке.

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

1 Ответ

2 голосов
/ 28 апреля 2020

Основная проблема, с которой вы сталкиваетесь, - это, возможно, удивительная совместимость типов таких значений, как x => x + 1 и i => i * 2 с и SyncFunc<number, number> и AsyncFunc<number, number>:

const sf: SyncFunc<number, number> = x => x + 1; // okay
const af: AsyncFunc<number, number> = x => x + 1; // also okay... what?!

Это работает, как задумано, но смущает достаточное количество людей, что является частью FAQ по TypeScript . Я адаптирую информацию, содержащуюся в этом вопросе:

В: Как x => x + 1 можно присвоить AsyncFunc, когда первый принимает один параметр, а второму требуется два параметра? Каким образом функция одного параметра может быть назначена функции двух параметров?

A: x => x + 1 является допустимым назначением для AsyncFunc, поскольку оно может безопасно игнорировать дополнительные параметры. Во время выполнения (x => x + 1)(10, "randomExtraParam") работает. Если сделать это ошибкой во время компиляции, это приведет к аннулированию многих традиционных методов, таких как Array.forEach() или Array.map(), реализация которых передает несколько параметров обратному вызову, но часто используется с обратными вызовами одного параметра. Вы можете прочитать запись FAQ, связанную выше, чтобы узнать, почему они считают это лучшим поведением здесь.

Q: Как можно x => x + 1 быть назначенным на AsyncFunc, когда первый возвращает number (при условии x выводится как number), а последний возвращает void? Как функция, возвращающая значение, может быть присвоена той, которая этого не делает?

A: Тип возврата void означает, что вызывающая сторона не может ожидать возврата значение, но это не значит, что точно не будет. Это просто означает, что вызывающая сторона должна игнорировать любое возвращаемое значение. Если вы игнорируете какое-либо возвращаемое значение, которое я вам даю, тогда не должно иметь значения, что я return 1 вместо return undefined. Если сделать это ошибкой, это приведет к аннулированию многих традиционных функций callback-acceptpting, которые не обращаются к возвращаемому значению обратного вызова, например Array.forEach(). Некоторые обратные вызовы, такие как a => arr.push(a), имеют как побочные эффекты, так и возвращаемые значения, а запрещение возвращаемых значений затруднит их использование обычными способами.


В свете этого поведение понятно; компилятор не может надежно различить guish между AsyncFunc<number, number> и SyncFunc<number, number>. Чтобы это произошло, вы должны изменить типы, чтобы они были несовместимы. Один из способов сделать это - сделать тип возвращаемого значения AsyncFunc отличным от void, например undefined:

type Resolve<T> = SyncFunc<T, undefined>;
type AsyncFunc<I, O> = (i: I, cb: Resolve<O>) => undefined;

Они похожи в том, что функция без return someValue оператор в конечном итоге вернет undefined, но компилятор теперь будет недоволен, если вы вернете 1 вместо undefined в AsyncFunc.

Это все еще не решает проблему различения функций по количеству параметров. Если вы просто напишите сигнатуру вызова map() как <I, O>(m: Func<I, O>): Pin<O, I>, то у компилятора технически достаточно информации, чтобы увидеть, что x => x + 1 должен быть SyncFunc, но, очевидно, слишком много, чтобы он мог сделать вывод сразу: он должен выведите как параметр типа I, так и тип x, но они зависят друг от друга, и он сдается. Иногда вы можете заставить его работать, но это не всегда стоит усилий. См. microsoft / TypeScript # 30134 для получения информации о проблеме и обсуждении, касающихся ограничений текущего алгоритма вывода типов и возможностей его улучшения.

На данный момент, однако, у вас уже есть обходной путь ведет себя так, как вы хотите: используйте перегрузки , чтобы сначала компилятор приложил серьезные усилия для интерпретации x => x + 1 как AsyncFunc, произвел сбой (из-за несовместимого типа возвращаемого значения), а затем попытался SyncFunc и добиться успеха. Это имеет ту же проблему, что и раньше, о I и x, но тип SyncFunc достаточно прост для работы (по сравнению с Func).

Это означает, что для вас работает следующее:

function map<I, O>(m: AsyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: SyncFunc<I, O>): Pin<O, I>;
function map<I, O>(m: Func<I, O>): Pin<O, I> {
    return new Pin<O, I>();
}

src<number>().to(map((i, c: Resolve<number>) => c(i * 2))).to(map(x => x + 1));
src<number>().to(map(i => i * 2)).to(map(x => x + 1));

Хорошо, надеюсь, это поможет; удачи!

Детская площадка ссылка на код

...