An open API service indexing awesome lists of open source software.

https://github.com/snowflyt/hkt-core

šŸƒ A micro HKT (higher-kinded type) implementation for TypeScript, with type safety elegantly guaranteed.
https://github.com/snowflyt/hkt-core

higher-kinded-types hkt type-level-programming typescript

Last synced: about 1 month ago
JSON representation

šŸƒ A micro HKT (higher-kinded type) implementation for TypeScript, with type safety elegantly guaranteed.

Awesome Lists containing this project

README

        


hkt-core


šŸƒ A micro HKT (higher-kinded type) implementation for TypeScript, with type safety elegantly guaranteed.



downloads


npm version


test status


MIT license

```typescript
/* Use as classical HKTs (e.g., @effect/typeclass or fp-ts style) */
interface MonadTypeClass {
of: (a: T) => Kind; // Lift a value into the monad
flatMap: (fa: Kind, f: (a: T) => Kind) => Kind;
}

// Create a `flatten` function for a monad from a monad type class
const createFlatten =
(monad: MonadTypeClass) =>
(ffa: Kind>): Kind =>
monad.flatMap(ffa, (x) => x);

/* Use as type-level functions (e.g., HOTScript style) */
type ConcatNames = Pipe<
Names, // [1]
Filter>>, // Filter out short names
Map, // Capitalize each name
JoinBy<", "> // Join names with a comma
>;

type _ = ConcatNames<["alice", "bob", "i"]>; // => "Alice, Bob"
```

[1]: This is just an example to demonstrate type-level functions. Some types used here (e.g., Filter, Map) are not built into hkt-core. See the following sections for more details.

## About

**Higher-Kinded Types (HKT)** are a powerful concept used in many popular TypeScript libraries, including [Effect](https://github.com/Effect-TS/website/blob/269d5065c5b548bc7fccc40b164dffcdb61b16bb/content/limbo/hkt.mdx), [fp-ts](https://github.com/gcanti/fp-ts/blob/669cd3ed7cb5726024331a7a1cf35125669feb30/src/HKT.ts#L7-L70), [TypeBox](https://github.com/sinclairzx81/typebox/blob/870ab417fb69775e3b490d4457aa5963b6f16673/src/type/schema/schema.ts#L52-L58) and [HOTScript](https://github.com/gvergnaud/hotscript/blob/0bc205286bd5eea0b89fa903c411df9aca95923c/src/internals/core/Core.ts#L29-L37). While these libraries share the core idea of HKTs, their detailed implementations differ, making it difficult to share HKTs across libraries seamlessly.

hkt-core solves this problem by providing a **standardized** and **type-safe** HKT implementation that works for **both classical HKT use cases** (like @effect/typeclass or fp-ts) and **type-level functions** (like HOTScript). Designed for easy integration with other libraries, it’s a **micro-library** that focuses solely on core HKT functionality without unnecessary extras.

Regarding the type-level functions use case, hkt-core also aims for **_zero-cost_ abstractions** — the type computations are **optimized** to be as efficient as possible. By using hkt-core, you get a more concise way to write type-level code without worrying about slowing down TypeScript's compilation.

## Installation

To install hkt-core via npm (or any other package manager you prefer):

```shell
npm install hkt-core
```

Alternatively, if you prefer a zero-dependency approach, you can directly _copy-and-paste_ `src/index.ts` into your project, which contains all hkt-core’s code in a single file. We guarantee **_no_ breaking changes** in **releases** _without_ a major version bump.

## Examples

hkt-core introduces some concepts that might take a little time to fully grasp. To get the most out of it, we recommend following the [quickstart guide](#quickstart) from start to finish. However, if you’re eager to jump straight into examples, we’ve provided a few here as TypeScript playground links. These examples will give you a quick overview of what hkt-core can do:

- [Create a monad typeclass with HKT](https://tsplay.dev/w2ypbW) (like in [@effect/typeclass](https://github.com/Effect-TS/effect/tree/596e051b0ced130899d35b32ed740e78326fd9a3/packages/typeclass) or [fp-ts](https://github.com/gcanti/fp-ts))
- [Composable type-level function programming with HKTs](https://tsplay.dev/NB94zw) (like in [HOTScript](https://github.com/gvergnaud/HOTScript), but in a type-safe way)
- [A type-level JSON parser with parser combinators](https://tsplay.dev/mbbKdm) (like in Haskell [Parsec](https://hackage.haskell.org/package/parsec))

## Quickstart

This section demonstrates how to use hkt-core in two common scenarios: **classical HKTs** (like in @effect/typeclass or fp-ts) and **type-level functions** (like in HOTScript).

### Use as classical HKTs 🐱

> [!TIP]
>
> This section assumes familiarity with **monads** and **type classes**. If you’re new to these concepts, we recommend checking out the [Effect documentation](https://github.com/Effect-TS/effect/blob/566236361e270e575ef1cbf308ad1967c82a362c/packages/typeclass/README.md) or the [fp-ts documentation](https://gcanti.github.io/fp-ts/) first — or feel free to skip to the next section, which is more beginner-friendly.

Let’s start with a **monad** example. A monad is a container type that supports `flatMap` (also known as `chain`) and `of` (also known as `pure` or `return`). For example, both `Array` and `Option` are monads because they support these operations. Since TypeScript doesn’t have a built-in `Option` type, let’s define one first:

```typescript
type Option = { _tag: "Some"; value: T } | { _tag: "None" };
const some = (value: T): Option => ({ _tag: "Some", value });
const none: Option = { _tag: "None" };
```

Next, let’s define `of` and `flatMap` for both `Array` and `Option`. We’ll use an object to represent a monad (a monad type class):

```typescript
const arrayMonad = {
of: (a: T) => [a],
flatMap: (fa: T[], f: (a: T) => U[]) => fa.flatMap(f),
};

const optionMonad = {
of: some,
flatMap: (fa: Option, f: (a: T) => Option) =>
fa._tag === "Some" ? f(fa.value) : none,
};
```

Now, let’s define a `flatten` function for a monad. Notice that `flatten` can be derived from `flatMap`:

```typescript
const flattenArray = (ffa: T[][]): T[] => arrayMonad.flatMap(ffa, (x) => x);
const flattenOption = (ffa: Option>): Option => optionMonad.flatMap(ffa, (x) => x);
```

To avoid writing separate `flatten` functions for each monad, we can create a `createFlatten` function that generates a `flatten` function from a monad:

```typescript
const createFlatten = (monad) => (ffa) => monad.flatMap(ffa, (x) => x);

const flattenArray = createFlatten(arrayMonad);
const flattenOption = createFlatten(optionMonad);
```

The challenge is how to type `createFlatten` correctly. Ideally, `createFlatten` should accept a monad type class for a generic monad type `F<~>`, where `F` is a higher-kinded type. If TypeScript supported higher-kinded types natively, we could write something like this:

```typescript
interface MonadTypeClass> {
of: (a: T) => F;
flatMap: (fa: F, f: (a: T) => F) => F;
}

const arrayMonad: MonadTypeClass> = /* ... */;
const optionMonad: MonadTypeClass> = /* ... */;

const createFlatten =
>(monad: MonadTypeClass) =>
(ffa: F>): F =>
monad.flatMap(ffa, (x) => x);
```

We can think of **HKTs** as functions that operate on types, or as type constructors in Haskell terms (represented as `* -> *`). For example:

- In Haskell, `Maybe` is a type constructor of kind `* -> *`. It takes a type `a` (like `Int`) and returns a new type `Maybe a` (like `Maybe Int`).
- Similarly, `List` is a type constructor of kind `* -> *`. It takes a type `a` and returns a new type `[a]` (a list of `a`).

In the code above, `F<~>` represents such a type constructor. The `MonadTypeClass` accepts a type constructor `F` and uses `F` to map a type `T` to a new type `F`. For example:

- If `F` is `Array`, then `F` is `Array`.
- If `F` is `Option`, then `F` is `Option`.

We have seen the power of HKTs in action. Unfortunately, TypeScript doesn’t natively support this syntax. However, hkt-core provides a way to simulate it:

```typescript
import { Apply, Arg0, TypeLambda1, Call1 } from "hkt-core";

// We use untyped `TypeLambda`s for now,
// see the next section for typed `TypeLambda`s
interface ArrayHKT extends TypeLambda1 {
return: Array>;
}
interface OptionHKT extends TypeLambda1 {
return: Option>;
}

type NumberArray = Apply; // => Array
type StringOption = Call1; // => Option
```

`TypeLambda`s are the core building blocks of hkt-core. They represent **type-level functions** that operate on types. Here, we use `TypeLambda1` because both `Array` and `Option` are type constructors that take **one type argument**. To extract the type arguments passed to a `TypeLambda`, we use utility types like `Args`, `Arg0`, `Arg1`, etc.

As shown above, we can ā€œinvokeā€ a `TypeLambda` with type arguments using `Apply` or its aliases like `Call1`, `Call2`, etc, which correspond to type-level functions that take exactly one, two, or more type arguments. These work similarly to `Function.prototype.apply` and `Function.prototype.call` in JavaScript.

For classical HKT use cases, hkt-core provides concise aliases like `HKT` and `Kind`, which can be seen as aliases for `TypeLambda1` and `Call1` (`Kind` is actually an alias for `Call1W`, see the [Aliases for classical HKT use cases](#aliases-for-classical-hkt-use-cases) section for details). Using these aliases, we can define a `MonadTypeClass` and `createFlatten` function like this:

```typescript
import { Arg0, HKT, Kind } from "hkt-core";

interface MonadTypeClass {
of: (a: T) => Kind;
flatMap: (fa: Kind, f: (a: T) => Kind) => Kind;
}

const createFlatten =
(monad: MonadTypeClass) =>
(ffa: Kind>): Kind =>
monad.flatMap(ffa, (x) => x);

interface ArrayHKT extends HKT {
return: Array>;
}
const arrayMonad: MonadTypeClass = {
of: (a) => [a],
flatMap: (fa, f) => fa.flatMap(f),
};

interface OptionHKT extends HKT {
return: Option>;
}
const optionMonad: MonadTypeClass = {
of: some,
flatMap: (fa, f) => (fa._tag === "Some" ? f(fa.value) : none),
};

const flattenArray = createFlatten(arrayMonad);
// ^?: (ffa: T[][]) => T[]
const flattenOption = createFlatten(optionMonad);
// ^?: (ffa: Option>) => Option
```

This code achieves the same functionality as the imaginary syntax above, but it works in real TypeScript. By defining `ArrayHKT` and `OptionHKT` and using `HKT` and `Kind`, we can simulate higher-kinded types effectively.

### Use as type-level functions ✨

hkt-core isn’t just for type constructors — it also supports **_typed_ type-level functions**, which go beyond `* -> *` to enable `TypeA -> TypeB` transformations. This makes it possible to combine _type-level_ functions with **type-safety**, including **_generic_ type-level functions**!

> [!TIP]
>
> **_Generic_ type-level functions** are a powerful feature and make up almost half of hkt-core’s codebase. However, due to their complexity, they are not covered in this quickstart guide. If you are curious, check out the [Generic type-level functions](#generic-type-level-functions) section after finishing this guide.

Let’s start with a JavaScript example: suppose we have an array of employee names, and we want to filter out names that are too short (which might be a bug in the data), capitalize the first letter of each name, and then join the names with a comma. We can write a function like this:

```typescript
const capitalize = (s: string) => (s.length > 0 ? s[0].toUpperCase() + s.slice(1) : "");

const concatNames = (names: string[]) =>
names
.filter((name) => name.length > 2)
.map(capitalize)
.join(", ");
```

In functional programming libraries like [Effect](https://github.com/Effect-TS/effect), this can be rewritten using **function composition**:

```typescript
import { pipe } from "effect";
import { filter, map } from "effect/Array";

const joinBy = (sep: string) => (strings: string[]) => strings.join(sep);

const concatNames = (names: string[]) =>
pipe(
names,
filter((name) => name.length > 2),
map(capitalize),
joinBy(", "),
);
```

Here, `filter`, `map`, and `join` are higher-order functions that return new unary functions, and `pipe` chains them together. For example, `pipe(value, f, g, h)` is equivalent to `h(g(f(value)))`. Similarly, `flow` can be used to create a composed function in Effect, e.g., `flow(f, g, h)` is equivalent to `(value) => h(g(f(value)))`.

But how can we implement such a function at **type level**? While the employee names example might seem trivial at type level, consider a real-world use case: replacing names with route paths, the predicate with a route prefix, and the join function with a router builder. This becomes a type-safe routing system! For now, let’s focus on the employee names example.

A practiced TypeScript developer might write the following type-level implementation:

```typescript
type FilterOutShortNames =
Names extends [infer Head extends string, ...infer Tail extends string[]] ?
Head extends `${infer A}${infer B}${infer C}` ?
"" extends A | B | C ?
FilterOutShortNames
: [Head, ...FilterOutShortNames]
: FilterOutShortNames
: [];

type CapitalizeNames = {
[K in keyof Names]: Capitalize;
};

type JoinNames =
Names extends [infer Head extends string, ...infer Tail extends string[]] ?
Tail extends [] ?
Head
: `${Head}, ${JoinNames}`
: "";

type ConcatNames = JoinNames>>;
```

While this works, it’s **_not_ reusable**. We can identify several common patterns here:

- **Filter tuple elements:** Recursive types with predicates, like `FilterOutShortNames`.
- **Map tuple elements:** Mapped types, like `CapitalizeNames`.
- **Reduce tuple elements:** Recursive types to reduce a tuple to a single value, like `JoinNames`.

If we continue writing such code, we’ll end up with a lot of boilerplate. Just as higher-order functions like `filter`, `map`, and `reduce` simplify JavaScript code, hkt-core enables us to implement these patterns with **type-level functions** in TypeScript. But before diving into implementing these familiar functions, let’s first explore how type-level functions work in hkt-core.

`TypeLambda`s are the core building blocks of hkt-core. They represent type-level functions that operate on types. To define a type-level function, we can create an interface extending `TypeLambda` and specify the `return` property, which describes how the input type is transformed:

```typescript
import { Apply, Arg0, Arg1, Call2, Sig, TypeLambda } from "hkt-core";

interface Concat extends TypeLambda<[s1: string, s2: string], string> {
return: `${Arg0}${Arg1}`;
}

// Use the `Sig` utility to check the signature of a type-level function
type ConcatSig = Sig; // => (s1: string, s2: string) => string

type _1 = Apply; // => "HelloWorld"
type _2 = Call2; // => "foobar"
```

Inside a `TypeLambda`, we can access the input types using `Args` and its variants like `Arg0`, `Arg1`, etc. To ā€œinvokeā€ a `TypeLambda`, we use `Apply` or its aliases like `Call1`, `Call2`, etc., which correspond to type-level functions that take exactly one, two, or more type arguments. These utilities work similarly to `Function.prototype.apply` and `Function.prototype.call` in JavaScript.

It’s worth noting that the `Concat` type-level function we created above is `typed`, meaning the input types are strictly checked. We declared the parameters as `[s1: string, s2: string]` and the return type as `string`. The parameters are represented as a [labeled tuple](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#labeled-tuple-elements) — these labels are just used by `Sig` to generate a human-readable signature and do not affect type checking or validation. You can remove them if you prefer.

If the input types don’t match the expected types, TypeScript will issue an error:

```typescript
type ConcatWrong1 = Apply;
// ~~~~~~~~~~~
// Type '["foo", 42]' does not satisfy the constraint '[s1: string, s2: string]'.
// Type at position 1 in source is not compatible with type at position 1 in target.
// Type 'number' is not assignable to type 'string'.

type ConcatWrong2 = Call2;
// ~~
// Type 'number' does not satisfy the constraint 'string'.
```

For more details on type checking and validation (e.g., how incompatible arguments are handled and how to bypass strict type checking), check out the [Type checking and validation in Detail](#type-checking-and-validation-in-detail) section.

hkt-core also provides type-level `Flow` and `Pipe` utility types to compose unary type-level functions. These types work similarly to `pipe` and `flow` in Effect or fp-ts:

```typescript
interface ConcatFoo extends TypeLambda<[s: string], string> {
return: `${Arg0}foo`;
}
interface ConcatBar extends TypeLambda<[s: string], string> {
return: `${Arg0}bar`;
}

type Composed = Flow;
type ComposedSig = Sig; // => (s: string) => string
type _1 = Call1; // => "hellofoobar"

type ConcatFooBar = Pipe;
type _2 = ConcatFooBar<"hello">; // => "hellofoobar"
```

`Flow` and `Pipe` supports up to 16 variadic type arguments, which should be sufficient for most use cases. Type checking is also performed on these utility types, ensuring that the input and output types match the expected types:

```typescript
interface Add1 extends TypeLambda<[n: number], number> {
return: [..._BuildTuple, void>, void]["length"];
}
type _BuildTuple =
[Length] extends [never] ? never
: Acc["length"] extends Length ? Acc
: _BuildTuple;

type ComposedWrong = Flow;
// ~~~~
// Type 'Add1' does not satisfy the constraint 'TypeLambda1'.
// Types of property 'signature' are incompatible.
// Type '(n: number) => number' is not assignable to type '(args_0: string) => any'.
// Types of parameters 'n' and 'args_0' are incompatible.
// Type 'string' is not assignable to type 'number'.

type ConcatFooBarWrong = Pipe;
// ~~~~
// Type 'Add1' does not satisfy the constraint 'TypeLambda1'.
// Types of property 'signature' are incompatible.
// Type '(n: number) => number' is not assignable to type '(args_0: string) => any'.
// Types of parameters 'n' and 'args_0' are incompatible.
// Type 'string' is not assignable to type 'number'.
```

While strict type checking on type-level functions might seem restrictive in simple examples, it becomes a powerful tool for catching errors early when working with more complex types.

Now let’s revisit the employee names example. With the knowledge we’ve gained, we can implement the type-level functions `Filter`, `Map`, and `Join`, and then compose them into a `ConcatNames` type-level function

```typescript
/* Define utility type-level functions */
interface NotExtend extends TypeLambda<[x: unknown], boolean> {
return: [Arg0] extends [U] ? false : true;
}

interface StringLength extends TypeLambda<[s: string], number> {
return: _StringLength>;
}
type _StringLength =
S extends `${string}${infer Tail}` ? _StringLength : Acc["length"];

interface CapitalizeString extends TypeLambda<[s: string], string> {
return: Capitalize>;
}

/* Define type-level functions for filtering, mapping and joining */
interface Filter>
extends TypeLambda<[xs: Param0[]], Param0[]> {
return: _Filter>;
}
type _Filter =
TS extends [infer Head, ...infer Tail] ?
Call1W extends true ?
_Filter
: _Filter
: Acc;

interface Map extends TypeLambda<[xs: Param0[]], RetType[]> {
return: _Map>;
}
type _Map = { [K in keyof TS]: Call1W };

interface JoinBy extends TypeLambda<[strings: string[]], string> {
return: Arg0 extends [infer S extends string] ? S
: Arg0 extends [infer Head extends string, ...infer Tail extends string[]] ?
`${Head}${Sep}${Call1, Tail>}`
: "";
}

/* We can use either `Flow` or `Pipe` to compose type-level functions */
type ConcatNamesFn = Flow<
Filter>>,
Map,
JoinBy<", ">
>;
type ConcatNamesSig = Sig; // => (xs: string[]) => string

type ConcatNames = Pipe<
Names,
Filter>>,
Map,
JoinBy<", ">
>;

/* Test the results! */
type Names = ["alice", "bob", "i", "charlie"];

type _1 = Call1; // => "Alice, "Bob", Charlie"
type _2 = ConcatNames; // => "Alice, "Bob", Charlie"
```

Some unfamiliar utility types are used in the example above:

- `Params` and its variants (`Param0`, `Param1`, etc.) are used to extract the **_declared_ parameters** of a `TypeLambda`.
- `RetType` is used to extract the **_declared_ return type** of a `TypeLambda`.

Don’t confuse these with the actual arguments passed to a `TypeLambda`, which are accessed using `Args` and its variants. You might notice that these type names are similar to `Parameters` and `ReturnType` in TypeScript — this is intentional to make them easier to remember.

We also use an interesting pattern to define types that ā€œreturnā€ a type-level function. For example, `Filter` and `JoinBy` are just simple type-level functions, but by using generic type parameters, we can ā€œinvokeā€ them with different types to create different type-level functions.

In the following sections, we’ll refer to these simple type-level functions with generic type parameters (like `Filter`, `Map`, and `JoinBy`) as ā€œ**type-level function templates**ā€. We’ll represent their signatures as:

- `Filter`: `[predicate: (value: T) => boolean](values: T[]) => T[]`
- `Map`: `[f: (value: T) => U](values: T[]) => U[]`
- `JoinBy`: `[sep: string](strings: string[]) => string`

Here, the part wrapped with `[...]` represents the generic type parameters, and the part wrapped with `(...)` represents the actual parameters. If you’re looking for a truly **_generic_ type-level function**, check out the [Generic Type-Level Functions](#generic-type-level-functions) section

## Documentation

### Generic type-level functions

While the ā€œ**type-level function templates**ā€ technique as described at the end of the [quickstart guide](#use-as-type-level-functions-) is useful in some cases, it has limitations. There’re times when a _truly_ **_generic_ type-level functions** is still unavoidable.

Let’s continue with the employee names example from the quickstart guide. Sometimes, the number of employees might be too large, and we only want to display the first 3 names. In common functional programming libraries, this can be achieved by using a function typically called `take`, which accepts a number `n` and a list of values, and returns the first `n` values of the list. We can define a type-level function `Take` as follows:

```typescript
interface Take extends TypeLambda<[values: any[]], any[]> {
return: _Take, N>;
}
type _Take =
TS extends [infer Head, ...infer Tail] ?
Counter["length"] extends N ?
[]
: [Head, ..._Take]
: [];

type TakeSig = Sig>; // => (values: any[]) => any[]
```

Since we haven’t yet introduced the concept of _generic_ type-level functions, we simply declare the signature of `Take` as `[n: number](values: any[]) => any[]`. Let’s use it to enhance the `ConcatNames` example:

```typescript
interface Append extends TypeLambda<[s: string], string> {
return: `${Arg0}${Suffix}`;
}

type ConcatNames = Flow<
Filter>>,
Take<3>,
Map,
JoinBy<", ">,
Append<", ...">
>;

type Names = ["alice", "bob", "i", "charlie", "david"];
type _ = Call1; // => "Alice, Bob, Charlie, ..."
```

This version works as expected, but we lose some type safety since the return type of `Take` is `any[]`. If we change `Map` to something like `Map>`, TypeScript will not catch the error:

```typescript
interface RepeatString extends TypeLambda<[n: number], string> {
return: _RepeatString>;
}
type _RepeatString =
[Times] extends [never] ? never
: Counter["length"] extends Times ? ""
: `${S}${_RepeatString}`;

type ConcatNames = Flow<
Filter>>,
Take<3>,
Map>,
JoinBy<", ">,
Append<", ...">
>;

// Unexpected result!
type _ = Call1; // => "${string}, ..."
```

We can declare `Take` as a **_generic_ type-level function** to ensure type safety:

```typescript
import type { Arg0, Sig, TArg, TypeLambdaG } from "hkt-core";

interface Take extends TypeLambdaG<["T"]> {
signature: (values: TArg[]) => TArg[];
return: _Take, N>;
}

type TakeSig = Sig>; // => (values: T[]) => T[]
```

Here, instead of extending `TypeLambda`, we extend `TypeLambdaG`, where the `G` suffix stands for ā€œ**generic**ā€. Instead of directly declaring the signature in `TypeLambda`, we declare the **type parameter list** in `TypeLambdaG` and use the `signature` property inside the function body to define the signature. All declared type parameters can be accessed using the `TArg` syntax within the `TypeLambdaG` body.

By defining `Take` as a _generic_ type-level function, TypeScript can now catch the error:

```typescript
type ConcatNames = Flow<
Filter>>,
Take<3>,
Map>,
// ~~~~~~~~~~~~~~~~~~~~~
// Type 'Map>' does not satisfy the constraint 'TypeLambda1'.
// Types of property 'signature' are incompatible.
// Type '(xs: number[]) => string[]' is not assignable to type '(args_0: string[]) => any'.
// Types of parameters 'xs' and 'args_0' are incompatible.
// Type 'string[]' is not assignable to type 'number[]'.
// Type 'string' is not assignable to type 'number'.
JoinBy<", ">,
Append<", ...">
>;
```

How does this work? Similar to generic functions in TypeScript, the **inference** mechanism of _generic_ type-level functions in hkt-core also relies on **type parameters**, which works as follows:

1. Try to infer the type parameters from all the parameter types or return types that are already known.
2. If a type parameter cannot be inferred, it defaults to its upper bound (`unknown` by default).
3. Replace all occurrences of type parameters in the signature with their actual types.

In the example above, we already know the type of the first parameter of `Take<3>` is `string[]` (from the previous type-level function `Filter>>`), so we can infer the type parameter `T` in `Take` as `string`. Then, we replace `TArg` with `string` in the signature of `Take`, inferring the return type as `string[]`. This allows TypeScript to catch the error when the next type-level function `Map` expects `number[]` but receives `string[]`.

How can `Flow` pass the ā€œknownā€ types to `Take` in order to return the correct type? Internally, it involves a utility type called `TypeArgs`, which accepts the second argument, `Known`, as the known types, and then gives the inferred type parameters:

```typescript
import type { TypeArgs } from "hkt-core";

type InferredTypeArgs1 = TypeArgs, [string[]]>; // => { readonly "~T": string }
type InferredTypeArgs2 = TypeArgs, { 0: number[] }>; // => { readonly "~T": number }
type InferredTypeArgs3 = TypeArgs, { r: boolean[] }>; // => { readonly "~T": boolean }
type InferredTypeArgs3 = TypeArgs, { 0: string[]; r: number[] }>; // => { readonly "~T": string | number }
```

Here, `Known` can be an object with integer keys and a special key `"r"` (tuples are also supported since they satisfy this condition), where the integer keys represent known parameter types at specific indexes, and `"r"` represents the known return type.

Utility types like `Params`, `RetType`, and their variants also support `Known` to provide a more precise result based on the known types:

```typescript
type InferredParams = Params, { r: string[] }>; // => [values: string[]]
type InferredRetType = RetType, { 0: string[] }>; // => string[]
```

The implementation of `Flow` relies on the second argument of `RetType` to compute a more precise return type of a type-level function based on the return type of the previous one, which is how type safety is achieved.

Now that we’ve explored how the **_generic_ type system** works in hkt-core, let’s look at the format of **_generic_ type parameters** in more detail:

```typescript
type GenericTypeParams = Array;
// A simple type parameter with only a name and the upper bound defaults to `unknown`
type SimpleTypeParam = `${Capitalize}`;
// A type parameter with its name (the first element) and an upper bound (the second element)
type TypeParamWithUpperBound = [`${Capitalize}`, unknown];
```

At the end of this section, let’s quickly skim some examples of other _generic_ type-level functions:

```typescript
// A type-level function that simply returns the input (this is already built into hkt-core)
interface Identity extends TypeLambdaG<["T"]> {
signature: (value: TArg) => TArg;
return: Arg0;
}

type IdentitySig = Sig; // => (value: T) => T

// A generic implementation of `Map`
interface Map extends TypeLambdaG<["T", "U"]> {
signature: (
f: TypeLambda<[x: TArg], TArg>,
xs: TArg[],
) => TArg[];
return: _Map, Arg1>;
}
type _Map = { [K in keyof TS]: Call1W };

type MapSig = Sig; // => (f: (x: T) => U, xs: T[]) => U[]

// A generic `Object.fromEntries` at type level
interface FromEntries extends TypeLambdaG<[["K", PropertyKey], "V"]> {
signature: (
entries: [TArg, TArg][],
) => Record, TArg>;
return: _FromEntries>;
}
type _FromEntries = _PrettifyObject<{
[K in Entries[number][0]]: Extract[1];
}>;
type _PrettifyObject = O extends infer U ? { [K in keyof U]: U[K] } : never;

type FromEntriesSig = Sig; // => (entries: [K, V][]) => Record
type _ = Call1; // => { name: string, age: number }
```

### Aliases for classical HKT use cases

hkt-core provide the following aliases for **type constructors**:

- `HKT`, `HKT2`, `HKT3` and `HKT4` are aliases for `TypeLambda1`, `TypeLambda2`, `TypeLambda3` and `TypeLambda4`, respectively.
- `Kind`, `Kind2`, `Kind3` and `Kind4` are aliases for `Call1W`, `Call2W`, `Call3W` and `Call4W`, respectively.

The aliases for higher-arity type constructors allow you to work with type constructors that take multiple type arguments, such as `Either` or `State`.

The `W` suffix in `Call*W` stands for ā€œ**widening**ā€, meaning type checking and validation are relaxed for arguments passed to the type-level function. For more details, see the [Bypass strict type checking and validation](#bypass-strict-type-checking-and-validation) sections.

### Type checking and validation in detail

#### Type checking V.S. Type validation

Just like in plain TypeScript, **type checking** and **type validation** are two different concepts that are often confused.

In plain TypeScript, **type checking** refers to the _compile-time_ verification that ensures variables, function parameters, and return values match their _declared_ types. Meanwhile, **(runtime) type validation** is the _run-time_ process that confirms actual values conform to the declared types. **Type checking** is handled by the TypeScript compiler, whereas **type validation** is usually performed by custom code or 3rd-party libraries such as [Zod](https://github.com/colinhacks/zod), [TypeBox](https://github.com/sinclairzx81/typebox) and [Arktype](https://github.com/arktypeio/arktype).

Although hkt-core is a _type-only_ library operating solely at _compile-time_, the distinction still applies. In hkt-core, **type checking** verifies that the input types provided to a type-level function are compatible with the _declared_ types, e.g., the TypeScript compiler will emit errors for signature mismatches in utilities like `Flow` or `Pipe`.

On the other hand, **type validation** in hkt-core ensures that the actual arguments passed or the computed return result match the _declared_ types, typically using utilities like `Args`, `Apply` and `Call*`. For example, if a `Concat` type-level function declared to return a `string` accidentally returns a `number`, the utility will yield `never` as the result without triggering a TypeScript error.

#### Bypass strict type checking and validation

There are cases where you might want to bypass strict type checking or validation, such as when working with complex generic types or when you need to handle incompatible types. hkt-core provides a set of utilities to help you handle these cases:

- `ApplyW`, `Call1W`, `Call2W`, etc. are the ā€œ**widening**ā€ versions of `Apply`, `Call1`, `Call2`, etc. They relax both type _checking_ for arguments passed to the type-level function **and type _validation_ for the return type**.
- `RawArgs` and its variants (`RawArg0`, `RawArg1`, etc.) are used to access the original arguments passed to a `TypeLambda`, regardless of whether they are compatible with the parameters.
- `Params`, `RetType`, `Args` and `RawArgs` all provide their **widening** versions (e.g., `RetTypeW`, `Args0W`, `RawArgs1W`, etc.) to bypass strict type checking. Unlike `ApplyW` and its variants, which relax both type _checking_ for arguments and type _validation_ for return types, these widening utilities are simple aliases for their strict counterparts that relaxes type checking. They return `never` when the input type is not a `TypeLambda`, and do not perform additional checks or relaxations.

Note that using `ApplyW` and its variants alone does not fully bypass strict type checking and validation if the body of a type-level function is still defined using `Args` and its variants. `ApplyW` and its variants only relax type _checking_ for arguments _passed_ to the type-level function and the return type, but they do not suppress type _validation_ performed by `Args` in the type-level function’s body. For example:

```typescript
interface Concat extends TypeLambda<[s1: string, s2: string], string> {
return: `${Arg0}${Arg1}`;
}

type ConcatWrong = ApplyW; // => never
```

Here, `ApplyW` still returns `never` because `42` is not compatible with `string`. To handle incompatible types, you can use `RawArgs` and its variants to access the original arguments:

```typescript
type Stringifiable = string | number | bigint | boolean | null | undefined;

interface Concat extends TypeLambda<[s1: string, s2: string], string> {
return: RawArg0 extends infer S1 extends Stringifiable ?
RawArg1 extends infer S2 extends Stringifiable ?
`${RawArg0}${RawArg1}`
: never
: never;
}

type ConcatWrong = ApplyW; // => "foo42"
```

However, you still need to manually check the types of `RawArg0` and `RawArg1` to ensure they are compatible with stringifiable types. Otherwise, TypeScript will issue an error:

```typescript
interface Concat extends TypeLambda<[s1: string, s2: string], string> {
return: `${RawArg0}${RawArg1}`;
// ~~~~~~~~~~~~~ ~~~~~~~~~~~~~
// Type 'RawArg0' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
// Type 'RawArg1' is not assignable to type 'string | number | bigint | boolean | null | undefined'.
}
```

While `ApplyW` might seem less useful in this example, it can be helpful in specific scenarios, such as when relaxing the return type of a type-level function:

```typescript
interface Concat extends TypeLambda<[s1: string, s2: string], number> {
// <- Return type is `number`
return: `${Arg0}${Arg1}`;
}

type _1 = Apply; // => never, since the declared return type is `number`
type _2 = ApplyW; // => "foobar", since the return type is relaxed
```

As we can see, bypassing strict type checking doesn’t always simplify things and can introduce additional complexity. These widening utilities are primarily intended for handling complex scenarios, such as when dealing with intricate variance or type constraints, and are not meant for common use cases. For example, they are useful when defining a `Flip` type-level function (already built into hkt-core) that swaps the order of two types.

In most cases, you don’t need these widening utilities if you skip declaring your type-level function’s signatures (i.e., use _untyped_ type-level functions). The parameters and return type of `TypeLambda` already default to `any`, so these widening utilities and their strict counterparts behave the same in such cases, as shown in the [Use as classical HKTs](#use-as-classical-hkts-) section.

#### Type validation in `Args`

`Args` and its variants (`Arg0`, `Arg1`, etc.) enforce strict type validation inside the `TypeLambda` definition. By using them, TypeScript can infer the types of the arguments against the **_declared_ parameters** correctly — meaning you don’t need to manually check the types of the arguments inside the `TypeLambda`, they just work!

```typescript
type JoinString = `${S1}${S2}`;
type JoinStringAndNumber = `${S}${N}`;

// This is not necessary
interface ConcatRedundant extends TypeLambda<[s1: string, s2: string], string> {
return: Arg0 extends infer S1 extends string ?
Arg1 extends infer S2 extends string ?
JoinString
: never
: never;
}

// This is enough
interface Concat extends TypeLambda<[s1: string, s2: string], string> {
return: JoinString, Arg1>; // OK
}

// Incompatible type errors are caught by TypeScript
interface ConcatMismatch extends TypeLambda<[s1: string, s2: string], string> {
// The intermediate error messages might be confusing, just focus on the last one for the actual issue
return: JoinStringAndNumber, Arg1>;
// ~~~~~~~~~~
// Type 'Arg1' does not satisfy the constraint 'number'.
// Type 'CastArgs>[1]' is not assignable to type 'number'.
// Type 'TolerantParams[1] | (AlignArgs<{}, TolerantParams, []> extends infer CastedArgs extends ExpectedParams ? CastedArgs : never)[1]' is not assignable to type 'number'.
// Type 'TolerantParams[1]' is not assignable to type 'number'.
// Type 'string' is not assignable to type 'number'.
}
```

What happens if you force-call a _typed_ type-level function with incompatible types? In such cases, incompatible arguments are replaced with `never`:

```typescript
interface Concat extends TypeLambda<[s1: string, s2: string], string> {
return: [Arg0, Arg1]; // We just print the arguments here for demonstration
}

type ConcatWrong = Call2W; // => ["foo", never]
```

The rules for handling incompatible arguments are as follows:

- If an argument is not compatible with the corresponding parameter, it is cast to `never`.
- Redundant arguments are truncated.
- Missing arguments are filled with `never`.

Here’s an example to demonstrate these rules:

```typescript
interface PrintArgs extends TypeLambda<[a: string, b: string], string> {
return: Args;
}

// Incompatible arguments are cast to `never`
type _1 = ApplyW; // => ["foo", never]
// Redundant arguments are truncated
type _2 = ApplyW; // => ["foo", "bar"]
// Missing arguments are filled with `never`
type _3 = ApplyW; // => ["foo", never]
```

If you want to access the original arguments passed to a `TypeLambda`, regardless of whether they are compatible with the parameters, use `RawArgs` or its variants instead (see the [Bypass strict type checking and validation](#bypass-strict-type-checking-and-validation) section for more details).

#### Type checking and validation in `Apply` and `Call*`

Just like `Args` and its variants, which coerce the arguments to match the declared parameters, `Apply` and its variants (`Call1`, `Call2`, etc.) coerce the returned value of a type-level function to match the declared return type. If the returned value is not compatible with the declared return type, it is cast to `never`:

```typescript
interface Concat extends TypeLambda<[s1: string, s2: string], string> {
return: `${Arg0}${Arg1}`;
}

// Here we return a number, which is incompatible with the declared return type `string`
interface ConcatWrong extends TypeLambda<[s1: string, s2: string], string> {
return: 42;
}

type _1 = Apply