23
loading...
This website collects cookies to deliver better user experience
declare const mapArray:
<A>(self: Array<A>, f: (a: A) => B) => Array<B>
declare const mapTree:
<A>(self: Tree<A>, f: (a: A) => B) => Tree<B>
declare const mapOption:
<A>(self: Option<A>, f: (a: A) => B) => Option<B>
Array|Tree|Option
.interface Mappable<F<~>> {
readonly map:
<A, B>(self: F<A>, f: (a: A) => B) => F<B>
}
declare const mapArray: Mappable<Array>["map"]
declare const mapTree: Mappable<Tree>["map"]
declare const mapOption: Mappable<Option>["map"]
declare const ArrayMappable: Mappable<Array>
declare const TreeMappable: Mappable<Tree>
declare const OptionMappable: Mappable<Option>
const stringify =
<F>(T: Mappable<F>) =>
(self: F<number>): F<string> =>
T.map(self, (n) => `number: ${n}`)
const stringifiedArray: Array<string> =
stringify(MappableArray)([0, 1, 2])
F<~>
is called a Higher Kinded Type
and interface Mappable<F<~>>
is called a TypeClass
.Option<~>
or Array<~>
, there are also data types with multiple parameters like Either<~, ~>
or Effect<~, ~, ~>
and we would also like to include those in the same setup, ideally we could do so by defining:interface Mappable<F<~, ~, ~>> {
readonly map: <R, E, A, B>(
self: F<R, E, A>,
f: (a: A) => B
) => F<R, E, B>
}
A
in Either
the first or the second parameter? we could have some conventions where E
is always the second and R
is always the first and A
always the last but that isn't too flexible too.F<~>
isn't even valid TypeScript, hopefully we got the idea of what we would like to have.this
type unification logic, given:interface MyInterface {
readonly x?: unknown
}
type X = (MyInterface & { readonly x: number })["x"]
X
to be number
given that unknown & number = number
.this
type so you can also expect:interface MyInterface {
readonly x?: unknown
readonly y: this["x"]
}
type Y = (MyInterface & { readonly x: number })["y"]
y
would be always unknown
but instead the this
parameter is special because it always represent the current type even in an extension chain X extends Y extends Z
, something defined as this
in Z
will appear as X
. That's fairly logical if you think about it for the usage in classes and interfaces for plain OOP inheritance.interface HKT {
// will reference the A type
readonly _A?: unknown
// will represent the computed type
readonly type?: unknown
}
Kind<F, A>
as a meaning for F<A>
, doing that is a little tricky:type Kind<F extends HKT, A> =
F extends {
readonly type: unknown
} ?
// F has a type specified, it is concrete (like F = ArrayHKT)
(F & {
readonly _A: A
})["type"] :
// F is generic, we need to mention all of the type parameters
// to guarantee that they are never excluded from type checking
{
readonly _F: F
readonly _A: () => A
}
interface ArrayHKT extends HKT {
readonly type: Array<this["_A"]>
}
type X = Kind<ArrayHKT, number>
X
is number[]
.Mappable
from before:interface Mappable<F extends HKT> {
readonly map:
<A, B>(self: Kind<F, A>, f: (a: A) => B) => Kind<F, B>
}
const MappableArray: Mappable<ArrayHKT>
MappableArray["map"]
and it will appear as <A, B>(self: A[], f: (a: A) => B) => B[]
.const stringify =
<F extends HKT>(T: Mappable<F>) =>
(self: Kind<F, number>): Kind<F, string> =>
T.map(self, (n) => `number: ${n}`)
Kind
has to mention all of the type parameters with their respective variance, so for example let's say we want to support up to 3 params one input and two outputs called R, E and A.interface HKT {
readonly _R?: unknown
readonly _E?: unknown
readonly _A?: unknown
readonly type?: unknown
}
type Kind<F extends HKT, R, E, A> =
F extends {
readonly type: unknown
} ?
(F & {
readonly _R: R
readonly _E: E
readonly _A: A
})["type"] :
{
readonly _F: F
readonly _R: (_: R) => void
readonly _E: () => E
readonly _A: () => A
}
interface Mappable<F extends HKT> {
readonly map:
<R, E, A, B>(
self: Kind<F, R, E, A>,
f: (a: A) => B
) => Kind<F, R, E, B>
}
interface ArrayHKT extends HKT {
readonly type: Array<this["_A"]>
}
const stringify =
<F extends HKT>(T: Mappable<F>) =>
<R, E>(self: Kind<F, R, E, number>) =>
T.map(self, (n) => `number: ${n}`)
declare const ArrayMappable: Mappable<ArrayHKT>
const res = stringify(ArrayMappable)([0, 1, 2])
Higher Kinded Types
and we were able to define a TypeClass
like Mappable
, to improve inference it is necessary to mention the F
parameter inside it otherwise it will be lost, we can do so by adding an optional parameter like:interface TypeClass<F extends HKT> {
readonly _F?: F
}
interface Mappable<F extends HKT> extends TypeClass<F> {
readonly map:
<R, E, A, B>(
self: Kind<F, R, E, A>,
f: (a: A) => B
) => Kind<F, R, E, B>
}
ValidationT
, ReaderT
, etc).