Что стоит у нас на пути, это когда вы пытаетесь передать переменную типа F
в качестве параметра типа другой переменной типа T
, например T<F>
, TS просто не позволяет этого, даже если вы знаете T
на самом деле является универсальным интерфейсом.
Существует обсуждение по этой теме, начиная с 2014 года, в выпуске github, и оно все еще открыто, поэтому команда TS, вероятно, не поддержит его в ближайшем будущем.
Термин для этой языковой функции называется тип с более высоким родом . Используя это ключевое слово для поиска, Google отправил меня в путешествие по кроличьей норе.
Оказывается, существует очень умный обходной путь!
Используя TS объединение деклараций (также известное как расширение модуля ), мы можем эффективно определить пустой интерфейс «хранилища типов», который действует как обычный объект, который содержит ссылку на другие полезные типы. Используя эту технику, мы можем преодолеть этот блокиратор!
Я буду использовать ваш случай в качестве примера, чтобы раскрыть идею этой техники. Если вы хотите погрузиться глубже, в конце я приведу несколько полезных ссылок.
Вот ссылка TS Playground ( оповещение спойлера ) на конечный результат. Вижу это вживую. Теперь давайте разберем его (или я должен сказать, построить его?) Шаг за шагом.
- Во-первых, давайте объявим пустой интерфейс
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>;
}
- Давайте также получим
keyof TypeStore
. Отмечено, что по мере обновления содержимого TypeStore
, $keys
также обновляется соответствующим образом.
type $keys = keyof TypeStore<any>
- Теперь мы исправляем отсутствующую языковую функцию «тип с более высоким родом», используя тип утилиты.
// 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
- Теперь у нас есть нужные инструменты, давайте начнем создавать полезные вещи.
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>
}
- Наконец
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);
Ссылка
- Дискуссия Github по поддержке более высокодушного типа в TS
- Вход в кроличью нору
- Декларация слияния в справочнике TS
- ТАК сообщение тип выше,
- Средний пост от @gcanti, на типах с более высоким родом в TS
fp-ts
lib by @ gcanti
hkts
lib by @ pelotom
typeprops
lib от @ SimonMeskens