| |

TypeScript 45 ๐Ÿ”ท Higher-Kinded Type Patterns

TypeScript does not have higher-kinded types. This is a fact, not a limitation of your understanding. Higher-kinded types โ€” the ability to abstract over a type constructor itself, as in F<T> where F is a parameter โ€” are present in Haskell, Scala, and Rust (via traits and associated types), but they were never added to TypeScript. The language has generics, so you can write Array<T>, Promise<T>, and Map<K, V>, but you cannot write a generic function that accepts Array or Promise as a parameter and applies it to a type. Array is not a type โ€” it is a type constructor, a function from types to types. TypeScript’s type system only quantifies over types, not over type constructors. The practical consequence is that patterns which would be one line in Haskell become workarounds in TypeScript. This chapter covers what higher-kinded types are, why they matter, the emulation tricks TypeScript developers use, and when those tricks are worth the complexity. It is an advanced topic, and much of the time the honest answer is: you do not need this. But knowing what it is and why TypeScript lacks it helps you recognize when you are fighting the language and when there is a clean encoding.

Key point: A higher-kinded type abstracts over a type constructor โ€” something like Array or Promise that takes a type and produces a type. TypeScript can abstract over types (<T>(x: T) => T), but not over type constructors (<F>(x: F<number>) => F<string> is not legal). The standard emulation uses a registry interface mapping names to type constructors, plus an Apply helper that looks up the constructor and applies it. This encoding works but requires declaration merging for each constructor and has real limitations โ€” inference is weaker, and the registry must be maintained. For most code, a plain generic or an overloaded function is simpler and clearer.


What higher-kinded types are

A kind is the “type of a type.” Ordinary types like string and number have kind * โ€” they are fully applied and describe values. Type constructors like Array and Promise have kind * -> * โ€” they take a type and produce a type. Array alone is not a type; Array<string> is. A higher-kinded type system lets you abstract over type constructors themselves, so a function or interface can say “for any F of kind * -> *, do something.”

Why this matters in practice. Consider a functor โ€” a container that supports mapping over its contents. Arrays are functors (map exists), promises are functors (then exists), and Option-like types are functors. In Haskell, you write one fmap that works for all of them:

fmap :: Functor f => (a -> b) -> f a -> f b

The f is a type constructor parameter. In TypeScript, you cannot write the equivalent:

// This is not valid TypeScript
function fmap<F, A, B>(f: (a: A) => B, fa: F<A>): F<B> {
  // ...
}

F<A> is a syntax error because F is not a type โ€” it is a type constructor, and TypeScript has no syntax for that. You can write fmap for arrays, and a separate one for promises, and a separate one for Option, but you cannot unify them under one generic signature.

Why TypeScript lacks HKT. Adding higher-kinded types would require a significant extension to the type system: kinds, kind inference, higher-kinded quantification, and the machinery to check that F is applied to the right number of arguments. The TypeScript team has repeatedly deferred this, citing complexity, performance, and the observation that most real TypeScript code does not need it. The language instead relies on structural typing and concrete generics, which cover the vast majority of cases.

What people do instead. They emulate HKT with a registry. The idea is to represent type constructors as entries in an interface, keyed by a name, and provide an Apply type that looks up a constructor and applies it. This is the encoding used by libraries like fp-ts and Effect. It works, but it is verbose and inference is limited.

Why the emulation is not really HKT. In a true HKT system, F is a genuine parameter that can be instantiated with any type constructor, including ones the compiler has never seen. In the emulation, F is a key into a registry that must be populated by declaration merging before use. Adding a new constructor means editing the registry. The abstraction leaks: it is not open in the way real HKT is.


The registry-and-Apply encoding

The standard emulation has three parts: a URItoKind interface that maps names to type constructors, a URI union of those names, and an Apply type that resolves a name to a concrete type.

interface URItoKind<A> {
  Array: Array<A>;
  Promise: Promise<A>;
}

type URIS = keyof URItoKind<unknown>;

type Apply<F extends URIS, A> = URItoKind<A>[F];

URItoKind<A> is a map from the name "Array" to the type Array<A>, and from "Promise" to Promise<A>. The Apply type indexes into this map with F to get the concrete type. Apply<"Array", string> resolves to Array<string>, and Apply<"Promise", number> resolves to Promise<number>.

Using the encoding. With Apply in place, you can write generic functions that abstract over the container.

function lift<F extends URIS, A>(fa: Apply<F, A>): Apply<F, A[]> {
  return [fa] as any; // simplified for illustration
}

The F parameter is a URI string, and Apply<F, A> is the container applied to A. This compiles, and it approximates the HKT pattern, but it requires a cast inside the implementation because the compiler cannot verify the body.

Declaration merging for extensibility. New constructors are added by merging into URItoKind.

interface URItoKind<A> {
  Option: Option<A>;
}

interface Option<A> {
  _tag: "Some" | "None";
  value: A;
}

After this merge, Apply<"Option", string> resolves to Option<string>. The registry grows by declaration merging, and the URIS union picks up the new key automatically because it is keyof URItoKind<unknown>.

Why the encoding is verbose. Every new container requires a registry entry, an interface, and often a helper. The any cast inside implementations is a constant reminder that the type system is not truly verifying the abstraction. This is the price of emulating a feature the language does not have.


Why URItoKind needs a type parameter

The URItoKind<A> interface is parameterized by A because the constructors are generic. Array takes an element type, so the map entry must be Array<A> where A is the parameter. This means each lookup applies the constructor to a specific A, which is exactly what Apply does.

Why one parameter is not always enough. Some constructors take two type parameters โ€” Map<K, V>, Either<E, A>, Result<T, E>. The simple registry handles only one parameter. For two-parameter constructors, the encoding needs a second registry or a way to partially apply.

interface URItoKind2<E, A> {
  Either: Either<E, A>;
  Result: Result<E, A>;
}

type Apply2<F extends keyof URItoKind2<unknown, unknown>, E, A> =
  URItoKind2<E, A>[F];

The two-parameter case gets its own registry and its own Apply2. This is the pattern fp-ts uses: URItoKind, URItoKind2, URItoKind3, and so on. Each arity needs its own machinery.

Why partial application is the hard part. In real HKT, you can partially apply a constructor: Either<E, _> is a type constructor waiting for one more argument. TypeScript has no syntax for partial application of a generic type. The workaround is to define a specialized type that fixes the first parameter:

type EitherE<E> = {
  kind: "Either";
  errorType: E;
};

This is not the same thing โ€” it is a marker, not a genuine partial application โ€” but it lets the registry key distinguish Either<string, _> from Either<number, _>. The complexity grows quickly.


When the emulation is worth it

The HKT emulation is a real tool with real use cases, but it is heavy. Knowing when to reach for it and when to avoid it is more important than knowing the encoding itself.

When it pays off:

  • You are building a library whose entire purpose is abstraction over containers โ€” fp-ts, Effect, io-ts, and similar
  • You have many operations (map, flatMap, traverse, sequence) that must work uniformly across many containers
  • The abstraction is the product, and users will bring their own containers
  • You control the registry and can document the merging requirement

When it is overkill:

  • You have two or three containers, and separate functions for each would be clearer
  • The abstraction is internal to one application, not exposed to users
  • You are the only consumer, and you know the concrete types
  • Inference matters more than uniformity โ€” the emulation weakens inference significantly

The pragmatic alternative. For most code, write the operation once per container or once per concrete type. A mapArray, a mapPromise, and a mapOption are three functions, each of which type-checks fully and infers correctly. The HKT version is one function with an any cast inside and weakened inference. The duplication is often the better trade.

Why functional programming libraries still use it. Because their entire reason for existing is uniformity across containers. fp-ts users expect map to work on Option, Either, Task, Reader, and everything else, with the same signature. That uniformity justifies the machinery. For application code that uses one or two containers, it does not.


Complete Example Session

// ============================================
// PART 1: THE LIMITATION
// ============================================

// This is not valid TypeScript:
// function fmap<F, A, B>(f: (a: A) => B, fa: F<A>): F<B> { ... }

// You can write concrete versions:
function mapArray<A, B>(f: (a: A) => B, fa: A[]): B[] {
  return fa.map(f);
}

function mapPromise<A, B>(f: (a: A) => B, fa: Promise<A>): Promise<B> {
  return fa.then(f);
}

// But you cannot unify them under one signature.

// ============================================
// PART 2: THE REGISTRY
// ============================================

interface URItoKind<A> {
  Array: Array<A>;
  Promise: Promise<A>;
}

type URIS = keyof URItoKind<unknown>;

type Apply<F extends URIS, A> = URItoKind<A>[F];

// ============================================
// PART 3: APPLY IN ACTION
// ============================================

type A1 = Apply<"Array", string>;     // string[]
type A2 = Apply<"Promise", number>;   // Promise<number>

// ============================================
// PART 4: A GENERIC FUNCTION
// ============================================

function lift<F extends URIS, A>(fa: Apply<F, A>): Apply<F, A[]> {
  // The cast is required; the compiler cannot verify the body.
  return [fa] as Apply<F, A[]>;
}

// ============================================
// PART 5: EXTENDING THE REGISTRY
// ============================================

interface Option<A> {
  _tag: "Some" | "None";
  value: A;
}

interface URItoKind<A> {
  Option: Option<A>;
}

// URIS now includes "Option" automatically.
type A3 = Apply<"Option", string>; // Option<string>

// ============================================
// PART 6: TWO-PARAMETER CONSTRUCTORS
// ============================================

interface Either<E, A> {
  _tag: "Left" | "Right";
  value: E | A;
}

interface URItoKind2<E, A> {
  Either: Either<E, A>;
}

type Apply2<F extends keyof URItoKind2<unknown, unknown>, E, A> =
  URItoKind2<E, A>[F];

type A4 = Apply2<"Either", string, number>; // Either<string, number>

// ============================================
// PART 7: THE COST โ€” WEAK INFERENCE
// ============================================

// The compiler infers F from the argument, but only from the registry.
// It cannot infer F from an arbitrary container type the way real HKT would.

// ============================================
// PART 8: THE ALTERNATIVE โ€” CONCRETE FUNCTIONS
// ============================================

// For a small number of containers, this is clearer:

function mapOption<A, B>(f: (a: A) => B, o: Option<A>): Option<B> {
  return o._tag === "Some"
    ? { _tag: "Some", value: f(o.value) }
    : { _tag: "None", value: undefined as never };
}

// ============================================
// PART 9: WHEN THE EMULATION EARNS ITS KEEP
// ============================================

// Uniform operations across many containers:
// - map, flatMap, traverse, sequence, chain
// - Each works the same way regardless of container
// - Users bring their own containers via declaration merging

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Do not reach for HKT emulation for two or three containers.
// Write concrete functions; they infer better and read better.

// Do not assume the emulation is sound.
// The `any` cast inside implementations is a real hole.

// Do not try to infer F from a container argument.
// The registry must be populated; inference is limited.

Each part isolates one aspect. Parts 1 through 5 show the basic encoding, parts 6 and 7 show the two-parameter case and the inference cost, and parts 8 through 10 show the alternative and the boundaries.


Quick Reference

What TypeScript Has vs Lacks

FeatureAvailable
Generic over a typeโœ… <T>(x: T) => T
Generic over a type constructorโŒ <F>(x: F<number>) => F<string>
Partial application of a genericโŒ
Kind annotationโŒ
HKT emulation via registryโœ… (with limitations)

The Encoding

PiecePurpose
URItoKind<A>Map names to constructors
URISUnion of names
Apply<F, A>Resolve a name to a concrete type
Declaration mergingAdd new constructors
URItoKind2<E, A>Two-parameter constructors
Apply2<F, E, A>Two-parameter application

Emulation vs Concrete

AspectEmulationConcrete
Uniformityโœ…โŒ
Inferenceโš ๏ธ Weakโœ… Strong
Soundnessโš ๏ธ any cast insideโœ…
Setup costHighLow
Best forLibraries over many containersApplication code

Libraries Using the Encoding

LibraryPurpose
fp-tsFunctional programming abstractions
EffectEffect system with HKT-like abstraction
io-tsRuntime type validation
fp-ts-contribAdditional HKT utilities

When to Use

SituationUse HKT emulation?
Library abstracting over many containersโœ…
Application with 2โ€“3 containersโŒ
One containerโŒ
Inference-critical codeโŒ
User-extensible abstractionโœ…

Best Practices

โœ… Do This:

// Use concrete functions when there are few containers
function mapArray<A, B>(f: (a: A) => B, fa: A[]): B[] {
  return fa.map(f);
}                                                              // โœ…

// Use the registry encoding only when uniformity is the product
interface URItoKind<A> { Array: Array<A>; Promise: Promise<A>; } // โœ…

// Keep the registry in one file, documented
// Add constructors via declaration merging                    // โœ…

// Prefer the library's encoding over hand-rolling it
// fp-ts and Effect already solve this                          // โœ…

// Name the cast site and comment it
return [fa] as Apply<F, A[]>; // compiler cannot verify        // โœ…

โŒ Don’t Do This:

// Don't write <F, A> F<A> โ€” it is not valid
// function fmap<F, A, B>(...) { }                              // โš ๏ธ

// Don't reach for HKT for two containers
// Write two functions instead                                  // โš ๏ธ

// Don't assume inference works like real HKT
// F must be inferable from the registry                        // โš ๏ธ

// Don't scatter `any` casts silently
// Each is a hole in the type system                            // โš ๏ธ

// Don't emulate partial application with markers unless needed
// It adds complexity that rarely pays off                      // โš ๏ธ

Common Pitfalls

PitfallProblemSolution
Writing F<A> directlyNot valid syntaxUse Apply<F, A>
Forgetting to merge registryApply fails to resolveAdd to URItoKind
Weak inferenceF not inferred from argumentAnnotate F explicitly
Two-parameter constructorsNo registry entryUse URItoKind2
Cast inside implementationUnsound bodyAcknowledge the hole
Overusing for small codeComplexity exceeds benefitUse concrete functions
Partial applicationNo native supportUse marker types
Assuming soundnessHKT emulation is not real HKTTreat as best-effort

Real-World Examples

1. Concrete map functions

function mapArray<A, B>(f: (a: A) => B, fa: A[]): B[] {
  return fa.map(f);
}

2. Registry for arrays and promises

interface URItoKind<A> {
  Array: Array<A>;
  Promise: Promise<A>;
}

3. Apply helper

type Apply<F extends URIS, A> = URItoKind<A>[F];

4. Extending the registry

interface URItoKind<A> {
  Option: Option<A>;
}

5. Two-parameter registry

interface URItoKind2<E, A> {
  Either: Either<E, A>;
}

6. Apply2

type Apply2<F extends keyof URItoKind2<unknown, unknown>, E, A> =
  URItoKind2<E, A>[F];

7. Option type

interface Option<A> {
  _tag: "Some" | "None";
  value: A;
}

8. Concrete option map

function mapOption<A, B>(f: (a: A) => B, o: Option<A>): Option<B> {
  return o._tag === "Some"
    ? { _tag: "Some", value: f(o.value) }
    : { _tag: "None", value: undefined as never };
}

9. Using fp-ts instead of hand-rolling

import { map } from "fp-ts/Array";

10. Type-level Apply for inference

function lift<F extends URIS, A>(fa: Apply<F, A>): Apply<F, A[]> {
  return [fa] as Apply<F, A[]>;
}

Visual: Kinds

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  KIND * (types of values)                    โ”‚
โ”‚                                              โ”‚
โ”‚    string      number      boolean           โ”‚
โ”‚    { x: 1 }   [1, 2, 3]    null              โ”‚
โ”‚                                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  KIND * -> * (type constructors)             โ”‚
โ”‚                                              โ”‚
โ”‚    Array<T>       Promise<T>                 โ”‚
โ”‚    Option<T>      Either<E, A>               โ”‚
โ”‚                                              โ”‚
โ”‚  These are not types until applied.          โ”‚
โ”‚  Array alone is not a type; Array<string> is.โ”‚
โ”‚                                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  HKT โ€” abstracting over * -> *               โ”‚
โ”‚                                              โ”‚
โ”‚    fmap<F, A, B>(f, F<A>) -> F<B>            โ”‚
โ”‚                                              โ”‚
โ”‚  TypeScript: โŒ not supported                โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: The Registry Encoding

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  interface URItoKind<A> {                    โ”‚
โ”‚    Array:   Array<A>;                        โ”‚
โ”‚    Promise: Promise<A>;                      โ”‚
โ”‚    Option:  Option<A>;                       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”‚
โ”‚  F โ”€โ”€โ”€โ”€โ–บโ”‚  URItoKind lookup    โ”‚โ”€โ”€โ”€โ”€โ–บ type   โ”‚
โ”‚         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ”‚
โ”‚                                              โ”‚
โ”‚  Apply<"Array", string>  โ†’  string[]         โ”‚
โ”‚  Apply<"Option", number> โ†’  Option<number>   โ”‚
โ”‚                                              โ”‚
โ”‚  The name F indexes the map.                 โ”‚
โ”‚  The map entry applies the constructor.      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Real HKT vs Emulation

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  REAL HKT (Haskell, Scala)                   โ”‚
โ”‚                                              โ”‚
โ”‚    fmap :: Functor f => (a -> b)             โ”‚
โ”‚              -> f a -> f b                   โ”‚
โ”‚                                              โ”‚
โ”‚  f is a genuine parameter.                   โ”‚
โ”‚  Any constructor can be used.                โ”‚
โ”‚  Inference works.                            โ”‚
โ”‚  Open โ€” new constructors need no registry.   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  EMULATION (TypeScript)                      โ”‚
โ”‚                                              โ”‚
โ”‚    function fmap<F extends URIS, A, B>(...)  โ”‚
โ”‚                                              โ”‚
โ”‚  F is a string key into a registry.          โ”‚
โ”‚  Only registered constructors work.          โ”‚
โ”‚  Inference is limited.                       โ”‚
โ”‚  Closed โ€” new constructors need merging.     โ”‚
โ”‚  `any` cast inside implementations.          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: When to Reach for the Encoding

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  How many containers?                        โ”‚
โ”‚                                              โ”‚
โ”‚       1           2-3           many         โ”‚
โ”‚       โ”‚            โ”‚              โ”‚          โ”‚
โ”‚       โ–ผ            โ–ผ              โ–ผ          โ”‚
โ”‚   concrete    concrete      HKT emulation    โ”‚
โ”‚   function    functions     (or a library)   โ”‚
โ”‚                                              โ”‚
โ”‚  Is the abstraction the product?             โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ No  โ”€โ”€โ–บ concrete functions         โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ Yes โ”€โ”€โ–บ HKT emulation              โ”‚
โ”‚                   (or use fp-ts / Effect)    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
HKT in TypeScriptNot supported
Abstraction over typesโœ… Generics
Abstraction over type constructorsโŒ
EmulationRegistry + Apply
RegistryURItoKind<A>
LookupApply<F, A>
ExtensionDeclaration merging
Two-parameterURItoKind2<E, A>
CostWeak inference, any casts
Best forLibraries over many containers
AlternativeConcrete functions per container

Key takeaways:

  • TypeScript does not have higher-kinded types โ€” you cannot abstract over a type constructor like Array or Promise
  • A higher-kinded type abstracts over type constructors, not over types, and TypeScript only supports the latter
  • The standard emulation uses a registry (URItoKind) plus an Apply helper that looks up a constructor and applies it
  • New constructors are added via declaration merging, and the URIS union picks them up automatically
  • Two-parameter constructors need a second registry โ€” URItoKind2 and Apply2
  • The emulation weakens inference and requires any casts inside implementations, which is a real cost
  • For a small number of containers, concrete functions are clearer, faster, and sound โ€” write mapArray, mapPromise, mapOption separately
  • The emulation earns its keep in libraries whose entire purpose is uniformity across many containers โ€” fp-ts, Effect, and similar
  • Know when you are fighting the language โ€” if you are hand-rolling HKT for three containers, you are probably writing more machinery than the problem deserves

Remember: Higher-kinded types are a real feature in some languages and a genuine gap in TypeScript. The registry encoding bridges the gap, but it is a workaround, not a solution โ€” inference is weaker, the registry must be maintained, and implementations rely on casts. Use it when the abstraction is the product. Otherwise, write concrete functions and move on.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!