Substitute Customized Interface Type Identifier

I am working on creating a versatile function interface for functor map while respecting the provided interface. In the code snippet below, my goal is to ensure that the value of mb is of type Maybe<number>, rather than the actual type Functor<number>.

I am aware that one solution could involve adding an overload to the FMap interface. However, I prefer not to go this route as I want this code to be part of a package where users can develop implementations for Functor and achieve the expected behavior when utilizing the map function.

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 am seeking a way to convey the following semantics:

// Hypothetical Code

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

Unfortunately, this approach does not work because a generic interface without a type parameter is invalid in TypeScript. For instance, an interface like Functor needs a type parameter to function as a type - it cannot stand alone as a valid type.

If there are no current ways to articulate these semantics, I would sincerely appreciate any suggestions for a solution that minimizes additional user-side coding.

Thank you for your time and consideration.

Answer №1

When attempting to pass a type variable F as a type parameter to another type variable T, such as T<F>, TypeScript (TS) prohibits this even when it is known that T is a generic interface.

A lingering discussion from 2014 on this subject in a GitHub issue remains unresolved, suggesting that the TS team may not provide support for it anytime soon.

This language feature is referred to as higher kinded type. A deep dive into this topic using Google led to interesting findings.

An ingenious workaround has been discovered!

By making use of TS's declaration merging (also known as module augmentation) feature, an empty "type store" interface can be defined effectively. This acts as a simple object holding references to other valuable types, enabling one to bypass this limitation!

Your specific case serves as an example to illustrate the concept behind this technique. For those interested in delving deeper, additional helpful links have been included at the end.

Here's the TS Playground link (spoiler alert) showcasing the final result. To fully grasp the concept, explore it live. Now let's dissect (or rather construct!) it step by step.

  1. To begin, define an empty TypeStore interface, which will be updated with content later on.
// treat it as a basic object
interface TypeStore<A> { } // why '<A>'? explained below


// exemplifying "declaration merging"
// instead of re-declaring the same interface,
// new members are added to dynamically update the interface
interface TypeStore<A> {
  Foo: Whatever<A>;
  Maybe: Maybe<A>;
}
  1. Obtain the keyof TypeStore as well. Note that as the contents of TypeStore evolve, $keys reflects these changes accordingly.
type $keys = keyof TypeStore<any>
  1. Incorporate a utility type to address the missing language feature of "higher kinded type."
// the '$' generic param signifies a unique symbol, not just 'string'
type HKT<$ extends $keys, A> = TypeStore<A>[$]

// instead of directly meaning `Maybe<A>`
// the syntax becomes:
HKT<'Maybe', A>  // once more, 'Maybe' isn't of string type but is a string literal
  1. Now equipped with the necessary tools, proceed to develop useful components.
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));
  }
}

// The crucial step!
// Introduce the newly declared class back into `TypeStore`
// providing it with a string literal key 'Maybe'
interface TypeStore<A> {
  Maybe: Maybe<A>
}
  1. Lastly, conclude with FMap:
// pivotal use of `infer $` here
// recall the initial obstacle faced?
// while direct inference of "Maybe from T and applying Maybe<A>" was impossible,
// inferring `$` and implementing "HKT<$, A>" became viable!
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);

References

  1. The ongoing conversation regarding higher kinded type adoption in TS on GitHub
  2. Entrance to a comprehensive exploration of higher kinded type in TypeScript
  3. Declaration Merging detailed in the TS Handbook
  4. Stack Overflow thread elucidating higher kinded type
  5. Insightful Medium post by @gcanti about higher kinded types in TS
  6. fp-ts library developed by @gcanti
  7. hkts library created by @pelotom
  8. typeprops library by @SimonMeskens

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Inputting Dates Manually in the Angular Material Datepicker Field

The datepicker function works well unless I manually type in the date. When I input a date between 01.MM.YYYY and 12.MM.YYYY, the value switches to MM.DD.YYYY. However, if I input 16.09.2021 for example, it remains as DD.MM.YYYY. Is there a way to change ...

Removing data based on various criteria in Prisma

While I understand that the where clause in Prisma requires a unique input for its delete operation, I have utilized the @@unique function to ensure that multiple conditions need to be columns together and must be unique. However, I am struggling with how ...

The TypeScript error states that the argument type 'string | undefined' cannot be assigned to the parameter type 'string'

Receiving TS error that says "Object is undefined" I am attempting to retrieve the "userid" from my headers. However, I keep encountering the error message "Argument of type 'string | undefined' is not assignable to parameter of type 'str ...

Struggling with object type casting in Typescript

Having issues with casting objects from an HTTP API response to Typescript. I am trying to cast the json data to a Typescript object using the "as" keyword or <Type >, but it's not working as expected. r.forEach(entry => { entry.creatio ...

You can't observe the behavior of simulated functions in a class with a manually created mock

Kindly note that I have set up a comprehensive Github repository where you can download and explore the content yourself here I am currently working on mocking a non-default exported class within a module using a manual mock placed in the folder __mocks__ ...

Display an error message in the input type file Form Control if the file format is not .doc or .docx

I need a way to display an alert if the user tries to submit a file that is not of type doc or docx. I've implemented a validator for this purpose and would like the alert message (Unacceptable file type) to be shown when the validation fails. Here i ...

Using the VSCode debugger to place a breakpoint within a Typescript package that has been symlinked using `npm link`

I'm currently troubleshooting a NodeJS application and its associated typescript packages, which have been linked using `npm link`. The directory structure is as follows: /root/package-a # typescript package /root/package-b # another typescript packa ...

Incorporating Sass into a TypeScript-enabled Create React App

Having recently transferred a create-react-app to typescript, I've encountered an issue where my scss files are not being recognized in the .tsx components. The way I'm importing them is as follows: import './styles/scss/style.scss'; ...

Implementing a Typescript directive in AngularJS to create a class

After using AngularJS for quite some time, I decided to explore Typescript. I managed to convert most of my Angular code to Typescript and found it beneficial, especially when working with services. However, I am struggling to convert the following direc ...

Can Angular 4 experience race conditions?

Here is a snippet of my Angular 4 Service code: @Injectable() export class MyService { private myArray: string[] = []; constructor() { } private calculate(result): void { myArray.length = 0; // Perform calculations and add results to myAr ...

Dealing with Angular 2's Http Map and Subscribe Problem

Looking to parse a JSON file and create a settingsProvider. This is how I am attempting it: import {Http} from "angular2/http"; import {Injectable} from "angular2/core"; @Injectable() export class SettingsProvider{ url: string = ""; constructor ...

Tips for effectively handling the data received from a webservice when logging into a system

My web service provides me with permissions from my user. The permissions are stored as an array in JSON format. I need to find a way to access and display this data in another function. {"StatusCode":0,"StatusMessage":"Authenticated Successfully", "Token ...

Exploring the concept of inheritance and nested views within AngularJS

I've encountered a challenge while setting up nested views in AngularJS. Utilizing the ui-router library has been beneficial, but I'm facing issues with separate controllers for each view without proper inheritance between them. This results in h ...

Modify animation trigger when mouse hovers over

I am looking to create a feature where a slide overlay appears from the bottom of a thumbnail when the user hovers over it, and retracts when the user is not hovering. animations: [ trigger('overlaySlide', [ state(&ap ...

When using the Angular Material table with selection enabled, the master toggle functionality should always deselect the

I made modifications to the original Angular Material Table example - Stackblitz. In my version, when some rows are selected and the master toggle is clicked, all selected rows should be deselected (similar to Gmail behavior). The functionality works, but ...

Unable to load class; unsure of origin for class labeled as 'cached'

Working on an Angular 10 project in visual studio code, I've encountered a strange issue. In the /app/_model/ folder, I have classes 'a', 'b', and 'c'. When running the application in MS Edge, I noticed that only classes ...

Is it feasible to broaden an interface in Typescript without including a specific type?

import React from "react"; interface a_to_e { a?: string; b?: string; c?: string; d?: string; e?: string; } interface a_to_e_without_c extends a_to_e { // I want to include properties a~e except for c } function Child(props: a_to_e_without_c ...

Using Typescript literal types as keys in an indexer is a common practice

Can we create a TypeScript literal type that acts as a string key in an indexer? type TColorKey = 'dark' | 'light'; interface ColorMap { [period: TColorKey]: Color; } But when attempting this, the following error is generated: An ...

Determine if a variable contains only one digit in Angular 6 using Typescript

I have a specific value that I want to discuss: this.value.day It gives me a numerical output ranging from 1 to 31. My requirement is to add a leading zero if the number is less than 10. Can anyone guide me on achieving this? ...

Show a notification pop-up when a observable encounters an error in an IONIC 3 app connected to an ASP.NET

Currently, I am in the process of developing an IONIC 3 application that consumes Asp.NET web API services. For authentication purposes, I have implemented Token based auth. When a user enters valid credentials, they receive a token which is then stored in ...