I am attempting to create a generic function interface for functor map, that respects the interface provided. In the code shown below, I would like the value of mb
to be of type Maybe<number>
, as opposed to the actual type Functor<number>
.
I do realize that one possible solution is to add an overload to the interface FMap
. The reason I am not happy with this solution is that I would like this code to reside in a package, allowing users to create implementations for Functor
, and have the behavior I described above when using the function 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);
I would like some means of expressing the following semantics:
// Theoretical Code
interface PretendFMap {
<A, B, FA extends Functor<A>>(fn: (a: A) => B, Fa: FA): FA extends (infer F)<A> ? F<B> : never;
}
This however does not function, as a generic interface, without a type parameter is not a valid TypeScript type, i.e. an interface such as Functor
requires a type parameter to be considered a type, Functor
itself is not a valid type.
If there are currently no means of expressing these semantics, any suggestions regarding a solution that requires as little code as possible on the side of the user would be greatly appreciated.
Thank you in advance for your time and consideration.
Generic Methods A type parameter, also known as a type variable, is an identifier that specifies a generic type name. The type parameters can be used to declare the return type and act as placeholders for the types of the arguments passed to the generic method, which are known as actual type arguments.
Generics make a class, interface and, method, consider all (reference) types that are given dynamically as parameters. This ensures type safety. Generic class parameters are specified in angle brackets “<>” after the class name as of the instance variable. Generic constructors are the same as generic methods.
A generic interface is primarily a normal interface like any other. It can be used to declare a variable but assigned the appropriate class. It can be returned from a method. It can be passed as argument. You pass a generic interface primarily the same way you would an interface.
Whenever you want to restrict the type parameter to subtypes of a particular class you can use the bounded type parameter. If you just specify a type (class) as bounded parameter, only sub types of that particular class are accepted by the current generic class.
The.NET class library defines several generic interfaces for use with the collection classes in the System.Collections.Generic namespace. When an interface is specified as a constraint on a type parameter, only types that implement the interface can be used.
An interface can define more than one type parameter, as follows: The rules of inheritance that apply to classes also apply to interfaces: Generic interfaces can inherit from non-generic interfaces if the generic interface is contravariant, which means it only uses its type parameter as a return value.
The preference for generic classes is to use generic interfaces, such as IComparable<T> rather than IComparable, in order to avoid boxing and unboxing operations on value types. The .NET Framework class library defines several generic interfaces for use with the collection classes in the System.Collections.Generic namespace.
Generic Type Parameters (C# Programming Guide) In a generic type or method definition, a type parameters is a placeholder for a specific type that a client specifies when they instantiate a variable of the generic type.
What stands in our way, is when you try to pass a type variable F
as type parameter to another type variable T
, like T<F>
, TS just doesn't allow that even if you know T
is in fact a generic interface.
There's a discussion on this topic dated back to 2014 in a github issue, and it's still open, so TS team probably won't support it in near future.
The term for this language feature is called higher kinded type. Using that search keyword, google took me to a trip down the rabbit hole.
It turns out theres exist a very clever workaround!
By leveraging TS declaration merging (aka module augmentation) feature, we can effectively define an empty "type store" interface, which acts like a plain object that holds reference to other useful types. Using this technique, we are able to overcome this blocker!
I'll use your case as example to cover the idea of this technique. If you want to dive deeper, I include some useful links at the end.
Here's the TS Playground link (spoiler alert) to the final result. See it in live for sure. Now let's break it down (or should I say build it up?) step by step.
TypeStore
interface, we'll update it's content later.// 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
. Noted that as content of TypeStore
gets updated, $keys
also get updated accordingly.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);
Reference
fp-ts
lib by @gcantihkts
lib by @pelotomtypeprops
lib by @SimonMeskensIf you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With