Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace Generic Interface Type Parameter

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.

like image 523
McPrescott Avatar asked Apr 15 '19 06:04

McPrescott


People also ask

What is a generic type parameter?

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.

Can we use generic in interface?

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.

What is a generic interface?

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.

How do I restrict a generic type in Java?

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.

What is a generic interface in system net?

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.

Can an interface have more than one type parameter?

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.

Why do we use generic interface instead of IComparable?

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.

What are generic type parameters?

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.


1 Answers

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.

  1. First, let's declare an empty 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>;
}
  1. Let's also get the keyof TypeStore. Noted that as content of TypeStore gets updated, $keys also get updated accordingly.
type $keys = keyof TypeStore<any>
  1. Now we patch up the missing language feature "higher kinded type", using a utility type.
// 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. Now we have the right tools, let's start building useful stuffs.
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. Finally 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

  1. The github discussion on supporting higer kinded type in TS
  2. Entrance to the rabbit hole
  3. Declaration Merging in TS Handbook
  4. SO post on higher kinded type
  5. Medium post by @gcanti, on higher kinded types in TS
  6. fp-ts lib by @gcanti
  7. hkts lib by @pelotom
  8. typeprops lib by @SimonMeskens
like image 107
hackape Avatar answered Sep 27 '22 19:09

hackape