Заменить параметр общего типа интерфейса - PullRequest
4 голосов
/ 15 апреля 2019

Я пытаюсь создать универсальный интерфейс функций для карты функторов, который учитывает предоставленный интерфейс. В приведенном ниже коде я бы хотел, чтобы значение mb было типа Maybe<number>, а не фактического типа Functor<number>.

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

.
interface Functor<A> {
  map<B>(fn: (a: A) => B): Functor<B>;
}

interface FMap {
  <A, B>(fn: (a: A) => B, Fa: Functor<A>): Functor<B>;
}

const map: FMap = (fn, Fa) => (
  Fa.map(fn)
);

class Maybe<A> implements Functor<A> {
  constructor(private readonly a: A) {}
  map<B>(fn: (a: A) => B): Maybe<B> {
    return new Maybe<B>(fn(this.a));
  }
}


const sqr = (x: number) => x*x;
const ma = new Maybe(5);
const mb = map(sqr, ma);

Я бы хотел выразить следующую семантику:

// Theoretical Code

interface PretendFMap {
  <A, B, FA extends Functor<A>>(fn: (a: A) => B, Fa: FA): FA extends (infer F)<A> ? F<B> : never;
}

Это, однако, не работает, так как универсальный интерфейс, без параметра типа, не является допустимым типом TypeScript, т. Е. Такой интерфейс, как Functor, требует, чтобы параметр типа считался типом, Functor сам по себе не является допустимый тип.

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

Заранее благодарим за ваше время и внимание.

1 Ответ

4 голосов
/ 15 апреля 2019

Что стоит у нас на пути, это когда вы пытаетесь передать переменную типа F в качестве параметра типа другой переменной типа T, например T<F>, TS просто не позволяет этого, даже если вы знаете T на самом деле является универсальным интерфейсом.

Существует обсуждение по этой теме, начиная с 2014 года, в выпуске github, и оно все еще открыто, поэтому команда TS, вероятно, не поддержит его в ближайшем будущем.

Термин для этой языковой функции называется тип с более высоким родом . Используя это ключевое слово для поиска, Google отправил меня в путешествие по кроличьей норе.

Оказывается, существует очень умный обходной путь!

Используя TS объединение деклараций (также известное как расширение модуля ), мы можем эффективно определить пустой интерфейс «хранилища типов», который действует как обычный объект, который содержит ссылку на другие полезные типы. Используя эту технику, мы можем преодолеть этот блокиратор!

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

Вот ссылка TS Playground ( оповещение спойлера ) на конечный результат. Вижу это вживую. Теперь давайте разберем его (или я должен сказать, построить его?) Шаг за шагом.

  1. Во-первых, давайте объявим пустой интерфейс TypeStore, позже мы обновим его содержимое.
// just think of it as a plain object
interface TypeStore<A> { } // why '<A>'? see below


// example of "declaration merging"
// it's not re-declaring the same interface
// but just adding new members to the interface
// so we can amend-update the interface dynamically
interface TypeStore<A> {
  Foo: Whatever<A>;
  Maybe: Maybe<A>;
}
  1. Давайте также получим keyof TypeStore. Отмечено, что по мере обновления содержимого TypeStore, $keys также обновляется соответствующим образом.
type $keys = keyof TypeStore<any>
  1. Теперь мы исправляем отсутствующую языковую функцию «тип с более высоким родом», используя тип утилиты.
// the '$' generic param is not just `string` but `string literal`
// think of it as a unique symbol
type HKT<$ extends $keys, A> = TypeStore<A>[$]

// where we mean `Maybe<A>`
// we can instead use:
HKT<'Maybe', A>  // again, 'Maybe' is not string type, it's string literal
  1. Теперь у нас есть нужные инструменты, давайте начнем создавать полезные вещи.
interface Functor<$ extends $keys, A> {
  map<B>(f: (a: A) => B): HKT<$, B>
}

class Maybe<A> implements Functor<'Maybe', A> {
  constructor(private readonly a: A) {}
  map<B>(f: (a: A) => B): HKT<'Maybe', B> {
    return new Maybe(f(this.a));
  }
}

// HERE's the key!
// You put the freshly declare class back into `TypeStore`
// and give it a string literal key 'Maybe'
interface TypeStore<A> {
  Maybe: Maybe<A>
}
  1. Наконец FMap:
// `infer $` is the key here
// remember what blocked us? 
// we cannot "infer Maybe from T" then apply "Maybe<A>"
// but we can "infer $" then apply "HKT<$, A>"!
interface FMap {
  <A, B, FA extends { map: Function }>
  (f: (a: A) => B, fa: FA): FA extends HKT<infer $, A> ? HKT<$, B> : any
}

const map: FMap = (fn, Fa) => Fa.map(fn);

Ссылка

  1. Дискуссия Github по поддержке более высокодушного типа в TS
  2. Вход в кроличью нору
  3. Декларация слияния в справочнике TS
  4. ТАК сообщение тип выше,
  5. Средний пост от @gcanti, на типах с более высоким родом в TS
  6. fp-ts lib by @ gcanti
  7. hkts lib by @ pelotom
  8. typeprops lib от @ SimonMeskens
...