├── .editorconfig ├── .githooks └── commit-msg ├── .github ├── ISSUE_TEMPLATE │ ├── 1 bug report.yml │ ├── 2 limitation discussion.yml │ ├── 3 new utility.yml │ ├── 4 type-level function share.yml │ └── 5 enhancement.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── commitlint.config.js ├── eslint.config.js ├── package-lock.json ├── package.json ├── prettier.config.cjs ├── src └── index.ts ├── test ├── Always.doc.test.ts ├── Apply.doc.test.ts ├── Args.doc.test.ts ├── Args.test.ts ├── Ask.doc.test.ts ├── Curry.doc.test.ts ├── Flip.doc.test.ts ├── Identity.doc.test.ts ├── Params.doc.test.ts ├── ParamsLength.doc.test.ts ├── README.test.ts ├── RetType.doc.test.ts ├── Sig.doc.test.ts ├── TolerantParams.doc.test.ts ├── TolerantRetType.doc.test.ts ├── Tupled.doc.test.ts ├── TypeArgs.doc.test.ts ├── TypeLambda.doc.test.ts ├── TypeLambdaG.doc.test.ts └── Untupled.doc.test.ts ├── tsconfig.json ├── typroof-plugin ├── index.ts ├── matchers.ts ├── package.json ├── plugin.ts └── validators.ts └── typroof.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js,cjs,mjs,ts,cts,mts,json}] 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.githooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npx --no -- commitlint --edit "$1" 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1 bug report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Report BUG 2 | description: Report a BUG for unexpected behavior 3 | labels: bug 4 | 5 | body: 6 | - type: textarea 7 | id: description 8 | validations: 9 | required: true 10 | attributes: 11 | label: BUG description 12 | 13 | - type: input 14 | id: repro 15 | validations: 16 | required: true 17 | attributes: 18 | label: Repro 19 | description: | 20 | Open [this TypeScript playground](https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBDAnmApnA3nAglA5gBgBps8BGYgYQEMAbGgJmIBVkUAZKkAIwBMq4AvnABmUCCDgAiABYBrGAFoAxtBSSA3AFgAUDuAA7GCijCqStBQj6lVeCgAeR-TwDOcFqg7c+AHgDaLqQAXHAuMFAGuMQu9CFhEfq4ALrR4ZEAfBg6cHBQKDAArlD6IQAGACToOAQ+MNLALukCldWktfWNAqVa2gI6OkiocJbWtgBKKC4FNPAAvMO0DD4jNjDEksIQEJLrXFRQkuk9APTHOXAAegD8OkA), write a piece of code that fails your expectations, click “Share”, and paste the URL here. 21 | 22 | - id: requirements 23 | type: checkboxes 24 | attributes: 25 | label: Search existing issues first 26 | options: 27 | - label: I tried my best to look for it 28 | required: true 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2 limitation discussion.yml: -------------------------------------------------------------------------------- 1 | name: 🤔 Discuss limitation 2 | description: Discuss a limitation that can be hardly solved with the current implementation of this library 3 | labels: limitation 4 | 5 | body: 6 | - type: textarea 7 | id: description 8 | validations: 9 | required: true 10 | attributes: 11 | label: Limitation description 12 | 13 | - type: input 14 | id: repro 15 | attributes: 16 | label: Repro 17 | description: | 18 | Open [this TypeScript playground](https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBDAnmApnA3nAglA5gBgBps8BGYgYQEMAbGgJmIBVkUAZKkAIwBMq4AvnABmUCCDgAiABYBrGAFoAxtBSSA3AFgAUDuAA7GCijCqStBQj6lVeCgAeR-TwDOcFqg7c+AHgDaLqQAXHAuMFAGuMQu9CFhEfq4ALrR4ZEAfBg6cHBQKDAArlD6IQAGACToOAQ+MNLALukCldWktfWNAqVa2gI6OkiocJbWtgBKKC4FNPAAvMO0DD4jNjDEksIQEJLrXFRQkuk9APTHOXAAegD8OkA), write a piece of code to show the limitation, click "Share", and paste the URL here. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3 new utility.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Suggest new utility type 2 | description: Only utilities that are widely used and have a clear use case are accepted 3 | labels: utility addition 4 | 5 | body: 6 | - type: textarea 7 | id: description 8 | validations: 9 | required: true 10 | attributes: 11 | label: Utility description + examples 12 | description: To keep this library small and focused, we only add utilities that are widely used and have a clear use case. Utilities for specific use cases or edge cases are not accepted. 13 | 14 | - type: textarea 15 | id: source 16 | attributes: 17 | label: Utility type source 18 | description: If you already have the utility type source, enter it here as a starting point for the discussion. 19 | 20 | - type: input 21 | id: playground 22 | attributes: 23 | label: TypeScript playground link 24 | description: | 25 | If you already have some code to demonstrate the utility type, open [this TypeScript playground](https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBDAnmApnA3nAglA5gBgBps8BGYgYQEMAbGgJmIBVkUAZKkAIwBMq4AvnABmUCCDgAiABYBrGAFoAxtBSSA3AFgAUDuAA7GCijCqStBQj6lVeCgAeR-TwDOcFqg7c+AHgDaLqQAXHAuMFAGuMQu9CFhEfq4ALrR4ZEAfBg6cHBQKDAArlD6IQAGACToOAQ+MNLALukCldWktfWNAqVa2gI6OkiocJbWtgBKKC4FNPAAvMO0DD4jNjDEksIQEJLrXFRQkuk9APTHOXAAegD8OkA), write a piece of code to show the proposed utility type behavior, click "Share", and paste the URL here. 26 | 27 | - id: requirements 28 | type: checkboxes 29 | attributes: 30 | label: Search existing types and issues first 31 | options: 32 | - label: I tried my best to look for it 33 | required: true 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4 type-level function share.yml: -------------------------------------------------------------------------------- 1 | name: 😺 Share your type-level function 2 | description: Share your type-level function that could benefit others! While we only accept widely-used utilities, your function may still be a valuable resource, even if not added to the library. 3 | labels: type-level function share 4 | 5 | body: 6 | - type: textarea 7 | id: description 8 | validations: 9 | required: true 10 | attributes: 11 | label: Description + examples 12 | 13 | - type: textarea 14 | id: source 15 | validations: 16 | required: true 17 | attributes: 18 | label: Type-level function source 19 | description: Provide the source code for your type-level function, either as a TypeScript snippet or a link to a GitHub repository or TypeScript playground. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/5 enhancement.yml: -------------------------------------------------------------------------------- 1 | name: ♻️ Propose change or improvement 2 | description: Propose a change or improvement to the library, such as new features, better documentation, or performance enhancements 3 | labels: enhancement 4 | 5 | body: 6 | - type: textarea 7 | id: description 8 | validations: 9 | required: true 10 | attributes: 11 | label: Description 12 | description: Describe the change or improvement you are proposing 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout the repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Lint 28 | run: npm run lint 29 | 30 | test: 31 | runs-on: ubuntu-latest 32 | 33 | strategy: 34 | matrix: 35 | node-version: [22.x] 36 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 37 | 38 | steps: 39 | - name: Checkout the repository 40 | uses: actions/checkout@v4 41 | 42 | - name: Set up Node.js ${{ matrix.node-version }} 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version: ${{ matrix.node-version }} 46 | cache: "npm" 47 | 48 | - name: Install dependencies 49 | run: npm ci --ignore-scripts 50 | 51 | - name: Test 52 | run: npm run test 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs/ 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | # Build 11 | node_modules/ 12 | dist/ 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea/ 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ge Gao (Snowflyt) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

hkt-core

2 | 3 |

4 | 🍃 A micro HKT (higher-kinded type) implementation for TypeScript, with type safety elegantly guaranteed. 5 |

6 | 7 |

8 | 9 | downloads 10 | 11 | 12 | npm version 13 | 14 | 15 | test status 16 | 17 | 18 | MIT license 19 | 20 |

21 | 22 | ```typescript 23 | /* Use as classical HKTs (e.g., @effect/typeclass or fp-ts style) */ 24 | interface MonadTypeClass { 25 | of: (a: T) => Kind; // Lift a value into the monad 26 | flatMap: (fa: Kind, f: (a: T) => Kind) => Kind; 27 | } 28 | 29 | // Create a `flatten` function for a monad from a monad type class 30 | const createFlatten = 31 | (monad: MonadTypeClass) => 32 | (ffa: Kind>): Kind => 33 | monad.flatMap(ffa, (x) => x); 34 | 35 | /* Use as type-level functions (e.g., HOTScript style) */ 36 | type ConcatNames = Pipe< 37 | Names, // [1] 38 | Filter>>, // Filter out short names 39 | Map, // Capitalize each name 40 | JoinBy<", "> // Join names with a comma 41 | >; 42 | 43 | type _ = ConcatNames<["alice", "bob", "i"]>; // => "Alice, Bob" 44 | ``` 45 | 46 | [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. 47 | 48 | ## About 49 | 50 | **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. 51 | 52 | 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. 53 | 54 | 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. 55 | 56 | ## Installation 57 | 58 | To install hkt-core via npm (or any other package manager you prefer): 59 | 60 | ```shell 61 | npm install hkt-core 62 | ``` 63 | 64 | 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. 65 | 66 | ## Examples 67 | 68 | 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: 69 | 70 | - [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)) 71 | - [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) 72 | - [A type-level JSON parser with parser combinators](https://tsplay.dev/mbbKdm) (like in Haskell [Parsec](https://hackage.haskell.org/package/parsec)) 73 | 74 | ## Quickstart 75 | 76 | 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). 77 | 78 | ### Use as classical HKTs 🐱 79 | 80 | > [!TIP] 81 | > 82 | > 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. 83 | 84 | 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: 85 | 86 | ```typescript 87 | type Option = { _tag: "Some"; value: T } | { _tag: "None" }; 88 | const some = (value: T): Option => ({ _tag: "Some", value }); 89 | const none: Option = { _tag: "None" }; 90 | ``` 91 | 92 | 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): 93 | 94 | ```typescript 95 | const arrayMonad = { 96 | of: (a: T) => [a], 97 | flatMap: (fa: T[], f: (a: T) => U[]) => fa.flatMap(f), 98 | }; 99 | 100 | const optionMonad = { 101 | of: some, 102 | flatMap: (fa: Option, f: (a: T) => Option) => 103 | fa._tag === "Some" ? f(fa.value) : none, 104 | }; 105 | ``` 106 | 107 | Now, let’s define a `flatten` function for a monad. Notice that `flatten` can be derived from `flatMap`: 108 | 109 | ```typescript 110 | const flattenArray = (ffa: T[][]): T[] => arrayMonad.flatMap(ffa, (x) => x); 111 | const flattenOption = (ffa: Option>): Option => optionMonad.flatMap(ffa, (x) => x); 112 | ``` 113 | 114 | To avoid writing separate `flatten` functions for each monad, we can create a `createFlatten` function that generates a `flatten` function from a monad: 115 | 116 | ```typescript 117 | const createFlatten = (monad) => (ffa) => monad.flatMap(ffa, (x) => x); 118 | 119 | const flattenArray = createFlatten(arrayMonad); 120 | const flattenOption = createFlatten(optionMonad); 121 | ``` 122 | 123 | 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: 124 | 125 | ```typescript 126 | interface MonadTypeClass> { 127 | of: (a: T) => F; 128 | flatMap: (fa: F, f: (a: T) => F) => F; 129 | } 130 | 131 | const arrayMonad: MonadTypeClass> = /* ... */; 132 | const optionMonad: MonadTypeClass> = /* ... */; 133 | 134 | const createFlatten = 135 | >(monad: MonadTypeClass) => 136 | (ffa: F>): F => 137 | monad.flatMap(ffa, (x) => x); 138 | ``` 139 | 140 | We can think of **HKTs** as functions that operate on types, or as type constructors in Haskell terms (represented as `* -> *`). For example: 141 | 142 | - 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`). 143 | - Similarly, `List` is a type constructor of kind `* -> *`. It takes a type `a` and returns a new type `[a]` (a list of `a`). 144 | 145 | 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: 146 | 147 | - If `F` is `Array`, then `F` is `Array`. 148 | - If `F` is `Option`, then `F` is `Option`. 149 | 150 | 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: 151 | 152 | ```typescript 153 | import { Apply, Arg0, TypeLambda1, Call1 } from "hkt-core"; 154 | 155 | // We use untyped `TypeLambda`s for now, 156 | // see the next section for typed `TypeLambda`s 157 | interface ArrayHKT extends TypeLambda1 { 158 | return: Array>; 159 | } 160 | interface OptionHKT extends TypeLambda1 { 161 | return: Option>; 162 | } 163 | 164 | type NumberArray = Apply; // => Array 165 | type StringOption = Call1; // => Option 166 | ``` 167 | 168 | `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. 169 | 170 | 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. 171 | 172 | For classical HKT use cases, hkt-core provides concise aliases like `HKT` and `Kind`, which can be seen as aliases for `TypeLambda1` and an enhanced version of `Call1`. Unlike `Call1W` which returns `never` when `F` is not a “concrete” type lambda, `Kind` uses `TolerantRetTypeW` as a fallback to provide a more useful default result. See the [Aliases for classical HKT use cases](#aliases-for-classical-hkt-use-cases) section for details. 173 | 174 | Using these aliases, we can define a `MonadTypeClass` and `createFlatten` function like this: 175 | 176 | ```typescript 177 | import { Arg0, HKT, Kind } from "hkt-core"; 178 | 179 | interface MonadTypeClass { 180 | of: (a: T) => Kind; 181 | flatMap: (fa: Kind, f: (a: T) => Kind) => Kind; 182 | } 183 | 184 | const createFlatten = 185 | (monad: MonadTypeClass) => 186 | (ffa: Kind>): Kind => 187 | monad.flatMap(ffa, (x) => x); 188 | 189 | interface ArrayHKT extends HKT { 190 | return: Array>; 191 | } 192 | const arrayMonad: MonadTypeClass = { 193 | of: (a) => [a], 194 | flatMap: (fa, f) => fa.flatMap(f), 195 | }; 196 | 197 | interface OptionHKT extends HKT { 198 | return: Option>; 199 | } 200 | const optionMonad: MonadTypeClass = { 201 | of: some, 202 | flatMap: (fa, f) => (fa._tag === "Some" ? f(fa.value) : none), 203 | }; 204 | 205 | const flattenArray = createFlatten(arrayMonad); 206 | // ^?: (ffa: T[][]) => T[] 207 | const flattenOption = createFlatten(optionMonad); 208 | // ^?: (ffa: Option>) => Option 209 | ``` 210 | 211 | 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. 212 | 213 | ### Use as type-level functions ✨ 214 | 215 | 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**! 216 | 217 | > [!TIP] 218 | > 219 | > **_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. 220 | 221 | 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: 222 | 223 | ```typescript 224 | const capitalize = (s: string) => (s.length > 0 ? s[0].toUpperCase() + s.slice(1) : ""); 225 | 226 | const concatNames = (names: string[]) => 227 | names 228 | .filter((name) => name.length > 2) 229 | .map(capitalize) 230 | .join(", "); 231 | ``` 232 | 233 | In functional programming libraries like [Effect](https://github.com/Effect-TS/effect), this can be rewritten using **function composition**: 234 | 235 | ```typescript 236 | import { pipe } from "effect"; 237 | import { filter, map } from "effect/Array"; 238 | 239 | const joinBy = (sep: string) => (strings: string[]) => strings.join(sep); 240 | 241 | const concatNames = (names: string[]) => 242 | pipe( 243 | names, 244 | filter((name) => name.length > 2), 245 | map(capitalize), 246 | joinBy(", "), 247 | ); 248 | ``` 249 | 250 | 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)))`. 251 | 252 | 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. 253 | 254 | A practiced TypeScript developer might write the following type-level implementation: 255 | 256 | ```typescript 257 | type FilterOutShortNames = 258 | Names extends [infer Head extends string, ...infer Tail extends string[]] ? 259 | Head extends `${infer A}${infer B}${infer C}` ? 260 | "" extends A | B | C ? 261 | FilterOutShortNames 262 | : [Head, ...FilterOutShortNames] 263 | : FilterOutShortNames 264 | : []; 265 | 266 | type CapitalizeNames = { 267 | [K in keyof Names]: Capitalize; 268 | }; 269 | 270 | type JoinNames = 271 | Names extends [infer Head extends string, ...infer Tail extends string[]] ? 272 | Tail extends [] ? 273 | Head 274 | : `${Head}, ${JoinNames}` 275 | : ""; 276 | 277 | type ConcatNames = JoinNames>>; 278 | ``` 279 | 280 | While this works, it’s **_not_ reusable**. We can identify several common patterns here: 281 | 282 | - **Filter tuple elements:** Recursive types with predicates, like `FilterOutShortNames`. 283 | - **Map tuple elements:** Mapped types, like `CapitalizeNames`. 284 | - **Reduce tuple elements:** Recursive types to reduce a tuple to a single value, like `JoinNames`. 285 | 286 | 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. 287 | 288 | `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: 289 | 290 | ```typescript 291 | import { Apply, Arg0, Arg1, Call2, Sig, TypeLambda } from "hkt-core"; 292 | 293 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 294 | return: `${Arg0}${Arg1}`; 295 | } 296 | 297 | // Use the `Sig` utility to check the signature of a type-level function 298 | type ConcatSig = Sig; // => (s1: string, s2: string) => string 299 | 300 | type _1 = Apply; // => "HelloWorld" 301 | type _2 = Call2; // => "foobar" 302 | ``` 303 | 304 | 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. 305 | 306 | 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. 307 | 308 | If the input types don’t match the expected types, TypeScript will issue an error: 309 | 310 | ```typescript 311 | type ConcatWrong1 = Apply; 312 | // ~~~~~~~~~~~ 313 | // Type '["foo", 42]' does not satisfy the constraint '[s1: string, s2: string]'. 314 | // Type at position 1 in source is not compatible with type at position 1 in target. 315 | // Type 'number' is not assignable to type 'string'. 316 | 317 | type ConcatWrong2 = Call2; 318 | // ~~ 319 | // Type 'number' does not satisfy the constraint 'string'. 320 | ``` 321 | 322 | 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. 323 | 324 | 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: 325 | 326 | ```typescript 327 | interface ConcatFoo extends TypeLambda<[s: string], string> { 328 | return: `${Arg0}foo`; 329 | } 330 | interface ConcatBar extends TypeLambda<[s: string], string> { 331 | return: `${Arg0}bar`; 332 | } 333 | 334 | type Composed = Flow; 335 | type ComposedSig = Sig; // => (s: string) => string 336 | type _1 = Call1; // => "hellofoobar" 337 | 338 | type ConcatFooBar = Pipe; 339 | type _2 = ConcatFooBar<"hello">; // => "hellofoobar" 340 | ``` 341 | 342 | `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: 343 | 344 | ```typescript 345 | interface Add1 extends TypeLambda<[n: number], number> { 346 | return: [..._BuildTuple, void>, void]["length"]; 347 | } 348 | type _BuildTuple = 349 | [Length] extends [never] ? never 350 | : Acc["length"] extends Length ? Acc 351 | : _BuildTuple; 352 | 353 | type ComposedWrong = Flow; 354 | // ~~~~ 355 | // Type 'Add1' does not satisfy the constraint 'TypeLambda1'. 356 | // Types of property 'signature' are incompatible. 357 | // Type '(n: number) => number' is not assignable to type '(args_0: string) => any'. 358 | // Types of parameters 'n' and 'args_0' are incompatible. 359 | // Type 'string' is not assignable to type 'number'. 360 | 361 | type ConcatFooBarWrong = Pipe; 362 | // ~~~~ 363 | // Type 'Add1' does not satisfy the constraint 'TypeLambda1'. 364 | // Types of property 'signature' are incompatible. 365 | // Type '(n: number) => number' is not assignable to type '(args_0: string) => any'. 366 | // Types of parameters 'n' and 'args_0' are incompatible. 367 | // Type 'string' is not assignable to type 'number'. 368 | ``` 369 | 370 | 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. 371 | 372 | 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 373 | 374 | ```typescript 375 | /* Define utility type-level functions */ 376 | interface NotExtend extends TypeLambda<[x: unknown], boolean> { 377 | return: [Arg0] extends [U] ? false : true; 378 | } 379 | 380 | interface StringLength extends TypeLambda<[s: string], number> { 381 | return: _StringLength>; 382 | } 383 | type _StringLength = 384 | S extends `${string}${infer Tail}` ? _StringLength : Acc["length"]; 385 | 386 | interface CapitalizeString extends TypeLambda<[s: string], string> { 387 | return: Capitalize>; 388 | } 389 | 390 | /* Define type-level functions for filtering, mapping and joining */ 391 | interface Filter> 392 | extends TypeLambda<[xs: Param0[]], Param0[]> { 393 | return: _Filter>; 394 | } 395 | type _Filter = 396 | TS extends [infer Head, ...infer Tail] ? 397 | Call1W extends true ? 398 | _Filter 399 | : _Filter 400 | : Acc; 401 | 402 | interface Map extends TypeLambda<[xs: Param0[]], RetType[]> { 403 | return: _Map>; 404 | } 405 | type _Map = { [K in keyof TS]: Call1W }; 406 | 407 | interface JoinBy extends TypeLambda<[strings: string[]], string> { 408 | return: Arg0 extends [infer S extends string] ? S 409 | : Arg0 extends [infer Head extends string, ...infer Tail extends string[]] ? 410 | `${Head}${Sep}${Call1, Tail>}` 411 | : ""; 412 | } 413 | 414 | /* We can use either `Flow` or `Pipe` to compose type-level functions */ 415 | type ConcatNamesFn = Flow< 416 | Filter>>, 417 | Map, 418 | JoinBy<", "> 419 | >; 420 | type ConcatNamesSig = Sig; // => (xs: string[]) => string 421 | 422 | type ConcatNames = Pipe< 423 | Names, 424 | Filter>>, 425 | Map, 426 | JoinBy<", "> 427 | >; 428 | 429 | /* Test the results! */ 430 | type Names = ["alice", "bob", "i", "charlie"]; 431 | 432 | type _1 = Call1; // => "Alice, "Bob", Charlie" 433 | type _2 = ConcatNames; // => "Alice, "Bob", Charlie" 434 | ``` 435 | 436 | Some unfamiliar utility types are used in the example above: 437 | 438 | - `Params` and its variants (`Param0`, `Param1`, etc.) are used to extract the **_declared_ parameters** of a `TypeLambda`. 439 | - `RetType` is used to extract the **_declared_ return type** of a `TypeLambda`. 440 | 441 | 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. 442 | 443 | 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. 444 | 445 | 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: 446 | 447 | - `Filter`: `[predicate: (value: T) => boolean](values: T[]) => T[]` 448 | - `Map`: `[f: (value: T) => U](values: T[]) => U[]` 449 | - `JoinBy`: `[sep: string](strings: string[]) => string` 450 | 451 | 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 452 | 453 | ## Documentation 454 | 455 | ### Generic type-level functions 456 | 457 | 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. 458 | 459 | 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: 460 | 461 | ```typescript 462 | interface Take extends TypeLambda<[values: any[]], any[]> { 463 | return: _Take, N>; 464 | } 465 | type _Take = 466 | TS extends [infer Head, ...infer Tail] ? 467 | Counter["length"] extends N ? 468 | [] 469 | : [Head, ..._Take] 470 | : []; 471 | 472 | type TakeSig = Sig>; // => (values: any[]) => any[] 473 | ``` 474 | 475 | 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: 476 | 477 | ```typescript 478 | interface Append extends TypeLambda<[s: string], string> { 479 | return: `${Arg0}${Suffix}`; 480 | } 481 | 482 | type ConcatNames = Flow< 483 | Filter>>, 484 | Take<3>, 485 | Map, 486 | JoinBy<", ">, 487 | Append<", ..."> 488 | >; 489 | 490 | type Names = ["alice", "bob", "i", "charlie", "david"]; 491 | type _ = Call1; // => "Alice, Bob, Charlie, ..." 492 | ``` 493 | 494 | 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: 495 | 496 | ```typescript 497 | interface RepeatString extends TypeLambda<[n: number], string> { 498 | return: _RepeatString>; 499 | } 500 | type _RepeatString = 501 | [Times] extends [never] ? never 502 | : Counter["length"] extends Times ? "" 503 | : `${S}${_RepeatString}`; 504 | 505 | type ConcatNames = Flow< 506 | Filter>>, 507 | Take<3>, 508 | Map>, 509 | JoinBy<", ">, 510 | Append<", ..."> 511 | >; 512 | 513 | // Unexpected result! 514 | type _ = Call1; // => "${string}, ..." 515 | ``` 516 | 517 | We can declare `Take` as a **_generic_ type-level function** to ensure type safety: 518 | 519 | ```typescript 520 | import type { Arg0, Sig, TArg, TypeLambdaG } from "hkt-core"; 521 | 522 | interface Take extends TypeLambdaG<["T"]> { 523 | signature: (values: TArg[]) => TArg[]; 524 | return: _Take, N>; 525 | } 526 | 527 | type TakeSig = Sig>; // => (values: T[]) => T[] 528 | ``` 529 | 530 | 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. 531 | 532 | By defining `Take` as a _generic_ type-level function, TypeScript can now catch the error: 533 | 534 | ```typescript 535 | type ConcatNames = Flow< 536 | Filter>>, 537 | Take<3>, 538 | Map>, 539 | // ~~~~~~~~~~~~~~~~~~~~~ 540 | // Type 'Map>' does not satisfy the constraint 'TypeLambda1'. 541 | // Types of property 'signature' are incompatible. 542 | // Type '(xs: number[]) => string[]' is not assignable to type '(args_0: string[]) => any'. 543 | // Types of parameters 'xs' and 'args_0' are incompatible. 544 | // Type 'string[]' is not assignable to type 'number[]'. 545 | // Type 'string' is not assignable to type 'number'. 546 | JoinBy<", ">, 547 | Append<", ..."> 548 | >; 549 | ``` 550 | 551 | 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: 552 | 553 | 1. Try to infer the type parameters from all the parameter types or return types that are already known. 554 | 2. If a type parameter cannot be inferred, it defaults to its upper bound (`unknown` by default). 555 | 3. Replace all occurrences of type parameters in the signature with their actual types. 556 | 557 | 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[]`. 558 | 559 | 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: 560 | 561 | ```typescript 562 | import type { TypeArgs } from "hkt-core"; 563 | 564 | type InferredTypeArgs1 = TypeArgs, [string[]]>; // => { readonly "~T": string } 565 | type InferredTypeArgs2 = TypeArgs, { 0: number[] }>; // => { readonly "~T": number } 566 | type InferredTypeArgs3 = TypeArgs, { r: boolean[] }>; // => { readonly "~T": boolean } 567 | type InferredTypeArgs3 = TypeArgs, { 0: string[]; r: number[] }>; // => { readonly "~T": string | number } 568 | ``` 569 | 570 | 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. 571 | 572 | Utility types like `Params`, `RetType`, and their variants also support `Known` to provide a more precise result based on the known types: 573 | 574 | ```typescript 575 | type InferredParams = Params, { r: string[] }>; // => [values: string[]] 576 | type InferredRetType = RetType, { 0: string[] }>; // => string[] 577 | ``` 578 | 579 | 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. 580 | 581 | 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: 582 | 583 | ```typescript 584 | type GenericTypeParams = Array; 585 | // A simple type parameter with only a name and the upper bound defaults to `unknown` 586 | type SimpleTypeParam = `${Capitalize}`; 587 | // A type parameter with its name (the first element) and an upper bound (the second element) 588 | type TypeParamWithUpperBound = [`${Capitalize}`, unknown]; 589 | ``` 590 | 591 | At the end of this section, let’s quickly skim some examples of other _generic_ type-level functions: 592 | 593 | ```typescript 594 | // A type-level function that simply returns the input (this is already built into hkt-core) 595 | interface Identity extends TypeLambdaG<["T"]> { 596 | signature: (value: TArg) => TArg; 597 | return: Arg0; 598 | } 599 | 600 | type IdentitySig = Sig; // => (value: T) => T 601 | 602 | // A generic implementation of `Map` 603 | interface Map extends TypeLambdaG<["T", "U"]> { 604 | signature: ( 605 | f: TypeLambda<[x: TArg], TArg>, 606 | xs: TArg[], 607 | ) => TArg[]; 608 | return: _Map, Arg1>; 609 | } 610 | type _Map = { [K in keyof TS]: Call1W }; 611 | 612 | type MapSig = Sig; // => (f: (x: T) => U, xs: T[]) => U[] 613 | 614 | // A generic `Object.fromEntries` at type level 615 | interface FromEntries extends TypeLambdaG<[["K", PropertyKey], "V"]> { 616 | signature: ( 617 | entries: [TArg, TArg][], 618 | ) => Record, TArg>; 619 | return: _FromEntries>; 620 | } 621 | type _FromEntries = _PrettifyObject<{ 622 | [K in Entries[number][0]]: Extract[1]; 623 | }>; 624 | type _PrettifyObject = O extends infer U ? { [K in keyof U]: U[K] } : never; 625 | 626 | type FromEntriesSig = Sig; // => (entries: [K, V][]) => Record 627 | type _ = Call1; // => { name: string, age: number } 628 | ``` 629 | 630 | ### Aliases for classical HKT use cases 631 | 632 | hkt-core provide the following aliases for **type constructors**: 633 | 634 | - `HKT`, `HKT2`, `HKT3` and `HKT4` are aliases for `TypeLambda1`, `TypeLambda2`, `TypeLambda3` and `TypeLambda4`, respectively. 635 | - `Kind`, `Kind2`, `Kind3` and `Kind4` are similar to `Call1W`, `Call2W`, `Call3W` and `Call4W`, but with an important enhancement: when `F` is not a “concrete” type lambda, they use `TolerantRetTypeW` as a fallback instead of returning `never`. This makes them more robust for classical HKT scenarios, such as when implementing type classes for higher-kinded types. 636 | 637 | The aliases for higher-arity type constructors allow you to work with type constructors that take multiple type arguments, such as `Either`, `State` or `Free`. 638 | 639 | 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. 640 | 641 | ### Type checking and validation in detail 642 | 643 | #### Type checking V.S. Type validation 644 | 645 | Just like in plain TypeScript, **type checking** and **type validation** are two different concepts that are often confused. 646 | 647 | 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). 648 | 649 | 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`. 650 | 651 | 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. 652 | 653 | #### Bypass strict type checking and validation 654 | 655 | 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: 656 | 657 | - `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**. 658 | - `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. 659 | - `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. 660 | 661 | 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: 662 | 663 | ```typescript 664 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 665 | return: `${Arg0}${Arg1}`; 666 | } 667 | 668 | type ConcatWrong = ApplyW; // => never 669 | ``` 670 | 671 | 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: 672 | 673 | ```typescript 674 | type Stringifiable = string | number | bigint | boolean | null | undefined; 675 | 676 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 677 | return: RawArg0 extends infer S1 extends Stringifiable ? 678 | RawArg1 extends infer S2 extends Stringifiable ? 679 | `${RawArg0}${RawArg1}` 680 | : never 681 | : never; 682 | } 683 | 684 | type ConcatWrong = ApplyW; // => "foo42" 685 | ``` 686 | 687 | 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: 688 | 689 | ```typescript 690 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 691 | return: `${RawArg0}${RawArg1}`; 692 | // ~~~~~~~~~~~~~ ~~~~~~~~~~~~~ 693 | // Type 'RawArg0' is not assignable to type 'string | number | bigint | boolean | null | undefined'. 694 | // Type 'RawArg1' is not assignable to type 'string | number | bigint | boolean | null | undefined'. 695 | } 696 | ``` 697 | 698 | 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: 699 | 700 | ```typescript 701 | interface Concat extends TypeLambda<[s1: string, s2: string], number> { 702 | // <- Return type is `number` 703 | return: `${Arg0}${Arg1}`; 704 | } 705 | 706 | type _1 = Apply; // => never, since the declared return type is `number` 707 | type _2 = ApplyW; // => "foobar", since the return type is relaxed 708 | ``` 709 | 710 | 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. 711 | 712 | 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. 713 | 714 | #### Type validation in `Args` 715 | 716 | `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! 717 | 718 | ```typescript 719 | type JoinString = `${S1}${S2}`; 720 | type JoinStringAndNumber = `${S}${N}`; 721 | 722 | // This is not necessary 723 | interface ConcatRedundant extends TypeLambda<[s1: string, s2: string], string> { 724 | return: Arg0 extends infer S1 extends string ? 725 | Arg1 extends infer S2 extends string ? 726 | JoinString 727 | : never 728 | : never; 729 | } 730 | 731 | // This is enough 732 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 733 | return: JoinString, Arg1>; // OK 734 | } 735 | 736 | // Incompatible type errors are caught by TypeScript 737 | interface ConcatMismatch extends TypeLambda<[s1: string, s2: string], string> { 738 | // The intermediate error messages might be confusing, just focus on the last one for the actual issue 739 | return: JoinStringAndNumber, Arg1>; 740 | // ~~~~~~~~~~ 741 | // Type 'Arg1' does not satisfy the constraint 'number'. 742 | // Type 'CastArgs>[1]' is not assignable to type 'number'. 743 | // Type 'TolerantParams[1] | (AlignArgs<{}, TolerantParams, []> extends infer CastedArgs extends ExpectedParams ? CastedArgs : never)[1]' is not assignable to type 'number'. 744 | // Type 'TolerantParams[1]' is not assignable to type 'number'. 745 | // Type 'string' is not assignable to type 'number'. 746 | } 747 | ``` 748 | 749 | What happens if you force-call a _typed_ type-level function with incompatible types? In such cases, incompatible arguments are replaced with `never`: 750 | 751 | ```typescript 752 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 753 | return: [Arg0, Arg1]; // We just print the arguments here for demonstration 754 | } 755 | 756 | type ConcatWrong = Call2W; // => ["foo", never] 757 | ``` 758 | 759 | The rules for handling incompatible arguments are as follows: 760 | 761 | - If an argument is not compatible with the corresponding parameter, it is cast to `never`. 762 | - Redundant arguments are truncated. 763 | - Missing arguments are filled with `never`. 764 | 765 | Here’s an example to demonstrate these rules: 766 | 767 | ```typescript 768 | interface PrintArgs extends TypeLambda<[a: string, b: string], string> { 769 | return: Args; 770 | } 771 | 772 | // Incompatible arguments are cast to `never` 773 | type _1 = ApplyW; // => ["foo", never] 774 | // Redundant arguments are truncated 775 | type _2 = ApplyW; // => ["foo", "bar"] 776 | // Missing arguments are filled with `never` 777 | type _3 = ApplyW; // => ["foo", never] 778 | ``` 779 | 780 | 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). 781 | 782 | #### Type checking and validation in `Apply` and `Call*` 783 | 784 | 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`: 785 | 786 | ```typescript 787 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 788 | return: `${Arg0}${Arg1}`; 789 | } 790 | 791 | // Here we return a number, which is incompatible with the declared return type `string` 792 | interface ConcatWrong extends TypeLambda<[s1: string, s2: string], string> { 793 | return: 42; 794 | } 795 | 796 | type _1 = Apply; // => "foobar" 797 | type _2 = Apply; // => never 798 | type _3 = Call2; // => never 799 | ``` 800 | 801 | In the example above, `ConcatWrong` returns a number, which is incompatible with the declared return type `string`. Even though `ConcatWrong` returns a value that is not `never` (i.e., `42`), `Apply` still coerces the returned value to `never` because it is not compatible with the declared return type. The same applies to the variants of `Apply`, such as `Call2` in this case. 802 | 803 | As is already mentioned in the [Bypass strict type checking and validation](#bypass-strict-type-checking-and-validation) section, you can use `ApplyW` and its variants if you don’t want the return value to be coerced to `never` when it’s incompatible with the declared return type. 804 | 805 | While the return type coercion behavior of `Apply` and its variants might cause confusion in some cases, it is useful for making TypeScript aware of type incompatibilities early on. For example, let’s revisit the `JoinBy` function from the `JoinBy` function from the [Generic type-level functions](#generic-type-level-functions) section. However, instead of using `Arg0 extends [infer S extends string]` in the body, let’s remove the `extends string` constraint and use `Arg0 extends [infer S]`: 806 | 807 | ```typescript 808 | interface JoinBy extends TypeLambda<[strings: string[]], string> { 809 | return: Arg0 extends [infer S] ? S 810 | : Arg0 extends [infer Head extends string, ...infer Tail extends string[]] ? 811 | `${Head}${Sep}${Call1, Tail>}` 812 | : ""; 813 | } 814 | ``` 815 | 816 | In this case, TypeScript cannot ensure that the return type of `JoinBy` is always `string`, because it cannot infer the type of `S` in the first condition, even though we know `S` can only be `string`. If we replace `Call1` with `Call1W`, we’ll actually get an error: 817 | 818 | ```typescript 819 | interface JoinBy extends TypeLambda<[strings: string[]], string> { 820 | return: Arg0 extends [infer S] ? S 821 | : Arg0 extends [infer Head extends string, ...infer Tail extends string[]] ? 822 | `${Head}${Sep}${Call1W, Tail>}` 823 | : // ~~~~~~~~~~~~~~~~~~~~~~~~~ 824 | // Type '...' is not assignable to type 'string | number | bigint | boolean | null | undefined'. 825 | // Type 'unknown' is not assignable to type 'string | number | bigint | boolean | null | undefined'. 826 | ""; 827 | } 828 | ``` 829 | 830 | However, because we use `Call1` in the original implementation, TypeScript doesn’t report such an issue in the function. This is because `Call1` always coerces the return value to match the declared return type `string`, allowing TypeScript to ensure that the type of `Call1, Tail>` must be `string`, and thus no issue arises. 831 | 832 | #### Type checking and validation in _generic_ type-level functions 833 | 834 | > [!TIP] 835 | > 836 | > This section assumes you’ve already read the [Generic type-level functions](#generic-type-level-functions) section. 837 | 838 | When it comes to **_generic_ type-level functions**, the type checking/validation behavior is slightly different. The general rule is _still the same_ as in previous sections, but here we have to address an issue that can often break type checking in many libraries: **variance**. 839 | 840 | Consider the generic `Map` example we skimmed at the end of the [Generic type-level functions](#generic-type-level-functions) section: 841 | 842 | ```typescript 843 | interface Map extends TypeLambdaG<["T", "U"]> { 844 | signature: ( 845 | f: TypeLambda<[x: TArg], TArg>, 846 | xs: TArg[], 847 | ) => TArg[]; 848 | return: _Map, Arg1>; 849 | } 850 | type _Map = { [K in keyof TS]: Call1W }; 851 | ``` 852 | 853 | Let’s try to implement a “type-safe” `MyApply` based on `ApplyW`, which you might initially write like this: 854 | 855 | ```typescript 856 | type MyApply> = ApplyW; 857 | ``` 858 | 859 | This works well for simple non-generic type-level functions, such as `Concat`, `Add`, or even type-level function templates like `JoinBy`. But when we apply it to `Map`, an issue arises: 860 | 861 | ```typescript 862 | type _ = MyApply, ["foo", "bar"]]>; 863 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 864 | // Type '[Append<"baz">, ["foo", "bar"]]' does not satisfy the constraint '[f: TypeLambda<[x: unknown], unknown>, xs: unknown[]]'. 865 | // Type at position 0 in source is not compatible with type at position 0 in target. 866 | // The types of 'signature' are incompatible between these types. 867 | // Type '(s: string) => string' is not assignable to type '(x: unknown) => unknown'. 868 | // Types of parameters 's' and 'x' are incompatible. 869 | // Type 'unknown' is not assignable to type 'string'. 870 | ``` 871 | 872 | This issue arises due to TypeScript's generic instantiation mechanism: when TypeScript cannot infer the type of a type parameter, it defaults to its upper bound (`unknown` by default). For those familiar with variance handling in TypeScript, this behavior may seem unintuitive: for covariant type parameters, this is expected, but for contravariant type parameters (like those in function parameters), they should default to `never`. This mismatch is why the issue occurs. 873 | 874 | Let’s take a look at the result of `Params` to better understand the root cause: 875 | 876 | ```typescript 877 | type ParamsOfMap = Params; // => [f: TypeLambda<[x: unknown], unknown>, xs: unknown[]] 878 | ``` 879 | 880 | The signature of `Append<"baz">` is `(s: string) => string`, whereas the expected signature is `(x: unknown) => unknown`. Because of the covariant nature of function parameters in TypeScript, `(s: string) => string` is not considered a subtype of `(x: unknown) => unknown`. This is because `unknown` is not assignable to `string`, which causes the type checking error. 881 | 882 | Similar issues also occur in non-type-level functions in TypeScript. For example: 883 | 884 | ```typescript 885 | const apply = unknown>(f: F, args: Parameters): ReturnType => 886 | Function.prototype.apply(f, args); 887 | 888 | const map = (f: (x: T) => U, xs: T[]): U[] => xs.map(f); 889 | 890 | apply(map, [(s: string) => s + "baz", ["foo", "bar"]]); 891 | // ~~~~~~~~~~~~~~~~~~~~~~~~ 892 | // Type '(s: string) => string' is not assignable to type '(x: unknown) => unknown'. 893 | // Types of parameters 's' and 'x' are incompatible. 894 | // Type 'unknown' is not assignable to type 'string'. 895 | ``` 896 | 897 | This explains why the signature of `Function.prototype.apply` in TypeScript is `(thisArg: any, argArray?: any) => any`: TypeScript cannot correctly handle the variance of function parameters, so it uses `any` to bypass strict type checking. 898 | 899 | However, in hkt-core, we need a solution for this problem because the only way to invoke a type-level function is through utilities like `Apply` and `Call*`. That’s where `TolerantParams` and `TolerantRetType` come in. They work as follows: 900 | 901 | 1. Test the variance of each type parameter (using dummy types): 902 | - If a type parameter is **covariant**, it is replaced with its **upper bound**. 903 | - If a type parameter is **contravariant**, it is replaced with `never`. 904 | - If a type parameter is **invariant**, it is replaced with `any`. 905 | 2. Replace the type parameters in the signature with the corresponding types. 906 | 3. Extract the parameters or return type from the signature. 907 | 908 | By applying these rules, `TolerantParams` and `TolerantRetType` yield more precise results than `Params` and `RetType` when used with generic type-level functions: 909 | 910 | ```typescript 911 | import type { TolerantParams, TolerantRetType } from "hkt-core"; 912 | 913 | type TolerantParamsOfMap = TolerantParams; // => [f: TypeLambda<[x: never], unknown>, xs: unknown[]] 914 | type TolerantRetTypeOfMap = TolerantRetType; // => unknown[] 915 | ``` 916 | 917 | With `TolerantParams` replacing `Params`, TypeScript no longer throws errors about incompatible types — and that’s how type checking for parameters is handled in `Apply` and its variants. 918 | 919 | The same principles apply to `Args` and its variants. `TolerantParams` is used instead of `Params` to ensure correct type validation, and we’ll skip the details here since they work in much the same way. 920 | 921 | ### Common Utilities 922 | 923 | There are many utilities commonly used in functional programming libraries. Some are very simple but are used frequently, while others may be used less often but can be quite complex to implement at the type level, especially to work well with _generic_ type-level functions. Some of these utilities are already built into hkt-core and can be seamlessly composed with your own type-level functions. 924 | 925 | #### `Always`, `Identity` and `Ask` 926 | 927 | These utilities are simple but frequently used in functional programming libraries. Let's start with `Always`, which creates a type-level function that accepts zero arguments and always returns a constant value (this function is often called `constant` in some libraries): 928 | 929 | ```typescript 930 | interface Always extends TypeLambda<[], T> { 931 | return: T; 932 | } 933 | ``` 934 | 935 | This utility is useful when a type-level function accepts a transformer function, like `TypeLambda1`, to transform the input type, but you don’t actually need to transform the input type and just want to return a constant value: 936 | 937 | ```typescript 938 | // Suppose we have a Rust-like `Result` type 939 | type Result = Ok | Err; 940 | // ... and a utility type-level function `Result.Match` to match the result 941 | namespace Result { 942 | export interface Match 943 | extends TypeLambda { 944 | return: /* ... */ 945 | } 946 | } 947 | 948 | type _ = Pipe, Result.Match, Always<"Oops!">>>; // => "Mr. Bob" 949 | ``` 950 | 951 | Next is `Identity`, a **_generic_ type-level function** that accepts a single argument and returns the same value: 952 | 953 | ```typescript 954 | interface Identity extends TypeLambdaG<["T"]> { 955 | signature: (value: TArg) => TArg; 956 | return: Arg0; 957 | } 958 | ``` 959 | 960 | `Identity` serves a similar role to `Always`, useful when you need to pass a transformer function but don’t want to transform the input type: 961 | 962 | ```typescript 963 | type MatchResult = Pipe>, Result.Match, Identity>>; 964 | ``` 965 | 966 | Another interesting use case of `Identity` is converting a `FlatMap` operation to `Flatten` by passing `Identity` as the transformer, and you can find even more use cases in practice 967 | 968 | Lastly, we have `Ask`, which behaves similarly to `Identity`, but instead of being a _generic_ type-level function, it is defined as a **type-level function template**: 969 | 970 | ```typescript 971 | interface Ask extends TypeLambda<[value: T], T> { 972 | return: Arg0; 973 | } 974 | ``` 975 | 976 | `Ask` might be the most useful utility among these three. It is commonly used with `Flow` or `Compose` to “pin” the signature of a type-level function to a specific type. For example: 977 | 978 | ```typescript 979 | import type { Ask, Compose, Flow, Identity } from "hkt-core"; 980 | 981 | type IdentityStringSig = Sig, Identity>>; // => (value: string) => string 982 | 983 | // You can also use `Compose`, which is used internally by `Flow`, 984 | // to compose exactly 2 type-level functions from **right to left** 985 | type IdentityNumberSig = Sig>>; // => (value: number) => number 986 | ``` 987 | 988 | When composing multiple type-level functions via `Flow`, if the first function is a _generic_ type-level function, you can use `Ask` to “pin” the first type-level function to avoid type errors about incompatible types. 989 | 990 | #### `Tupled` and `Untupled` 991 | 992 | `Flow` and `Pipe` are incredibly useful for composing multiple type-level functions together, but they only work with functions that accept a single argument. If you have a type-level function that accepts multiple arguments, you can use `Tupled` to convert it into a function that accepts a single tuple argument: 993 | 994 | ```typescript 995 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 996 | return: `${Arg0}${Arg1}`; 997 | } 998 | type ConcatSig = Sig; // => (s1: string, s2: string) => string 999 | type _1 = Call2; // => "foobar" 1000 | 1001 | type TupledConcat = Tupled; 1002 | type TupledConcatSig = Sig; // => (args: [s1: string, s2: string]) => string 1003 | type _2 = Call1; // => "foobar" 1004 | ``` 1005 | 1006 | The inverse of `Tupled` is `Untupled`, which converts a function that accepts a single tuple argument back into a function that accepts multiple arguments: 1007 | 1008 | ```typescript 1009 | interface First extends TypeLambdaG<["T"]> { 1010 | signature: (pair: [TArg, unknown]) => TArg; 1011 | return: Arg0[0]; 1012 | } 1013 | type FirstSig = Sig; // => (pair: [T, unknown]) => T 1014 | type _1 = Call1; // => 42 1015 | 1016 | type UntupledFirst = Untupled; 1017 | type UntupledFirstSig = Sig; // => (args_0: T, args_1: unknown) => T 1018 | type _2 = Call2; // => 42 1019 | ``` 1020 | 1021 | #### `Flip` 1022 | 1023 | `Flip` is useful when you need to swap the order of the arguments in a `TypeLambda2`. It is already built into hkt-core and can be used as follows: 1024 | 1025 | ```typescript 1026 | interface Map extends TypeLambdaG<["T", "U"]> { 1027 | signature: ( 1028 | f: TypeLambda<[x: TArg], TArg, 1029 | xs: TArg[], 1030 | ) => TArg[]; 1031 | return: _Map, Arg1>; 1032 | } 1033 | type _Map = { [K in keyof TS]: Call1W }; 1034 | 1035 | type MapSig = Sig; // => (f: (x: T) => U, xs: T[]) => U[] 1036 | type _1 = Call2, ["foo", "bar"]>; // => ["foobaz", "barbaz"] 1037 | 1038 | type FlippedMap = Flip; 1039 | type FlippedMapSig = Sig; // => (xs: U[], f: (x: U) => T) => T[] 1040 | type _2 = Call2>; // => ["foobaz", "barbaz"] 1041 | ``` 1042 | 1043 | As you can see, `Flip` swaps the order of the arguments in the function. In this example, the `Map` function originally expects the function (`f`) to be the first argument, and the array (`xs`) to be the second. After using `Flip`, the arguments are reversed so that the array comes first, followed by the function. 1044 | 1045 | `Flip` also works with **curried** binary type-level functions (i.e., `TypeLambda1`): 1046 | 1047 | ```typescript 1048 | // See the next section for `Curry` 1049 | type CurriedMap = Curry; 1050 | type CurriedMapSig = Sig; // => (f: (x: T) => U) => (xs: T[]) => U[] 1051 | type _1 = Call1>, ["foo", "bar"]>; // => ["foobaz", "barbaz"] 1052 | 1053 | type FlippedCurriedMap = Flip; 1054 | type FlippedCurriedMapSig = Sig; // => (xs: T[]) => (f: (x: T) => U) => U[] 1055 | type _2 = Call1, Append<"baz">>; // => ["foobaz", "barbaz"] 1056 | ``` 1057 | 1058 | #### `Curry` 1059 | 1060 | Currying is a common technique in functional programming that transforms a function accepting multiple arguments into a series of functions that each accept a single argument. While useful, currying doesn't fit well in TypeScript, especially **auto-currying** (see [a discussion about TypeScript support in Ramda’s repository](https://github.com/ramda/ramda/issues/2976#issuecomment-706475091)). It’s also quite challenging to create a type-safe, general-purpose `curry` function that works well with generics. 1061 | 1062 | However, it _is_ possible to create type-safe curry functions for a specific number of arguments (e.g., `curry2`, `curry3`, etc.), or use overloads for each number of arguments to create a close-to-general-purpose, type-safe `curry` function. hkt-core provides a utility called `Curry` that supports up to **3** arguments, which can be thought of as a combination of overloads for `curry2` and `curry3`. 1063 | 1064 | For example, you can curry the previously defined `Map` function like this: 1065 | 1066 | ```typescript 1067 | type CurriedMap = Curry; 1068 | type CurriedMapSig = Sig; // => (f: (x: T) => U) => (xs: T[]) => U[] 1069 | type _ = Call1>, ["foo", "bar"]>; // => ["foobaz", "barbaz"] 1070 | ``` 1071 | 1072 | You can also create a curried version of `Reduce`: 1073 | 1074 | ```typescript 1075 | interface Reduce extends TypeLambdaG<["T", "U"]> { 1076 | signature: ( 1077 | f: TypeLambda<[acc: TArg, x: TArg], TArg>, 1078 | init: TArg, 1079 | xs: TArg[], 1080 | ) => TArg; 1081 | return: _Reduce, Arg1, Arg2>; 1082 | } 1083 | type ReduceSig = Sig; // => (f: (acc: U, x: T) => U, init: U, xs: T[]) => U 1084 | type _1 = Call3; // => "foobarbaz" 1085 | 1086 | type CurriedReduce = Curry; 1087 | type CurriedReduceSig = Sig; // => (f: (acc: U, x: T) => U) => (init: U) => (xs: T[]) => U 1088 | type _2 = Call1, "">, ["foo", "bar", "baz"]>; // => "foobarbaz" 1089 | ``` 1090 | 1091 | `Curry` is also quite useful in combination with `Flip`. If you have a binary type-level function like `Map`, you can use `Curry` and `Flip` to create two type-level function templates with different argument orders: 1092 | 1093 | ```typescript 1094 | // [f: (x: T) => U](xs: T[]) => U[] 1095 | type MapBy = Call1, F>; 1096 | type MapBySig = Sig>>; // => (xs: string[]) => string[] 1097 | 1098 | // [xs: T[]](f: (x: T) => U) => U[] 1099 | type MapOn = Call1>, TS>; 1100 | type MapOnSig = Sig>; // => (f: (x: string) => unknown) => unknown[] 1101 | ``` 1102 | 1103 | It’s worth noting that `U` is widened to `unknown` in the `MapOn` example. This is a limitation of TypeScript’s type inference system, and you’ll encounter the same issue if you define similar non-type-level `flip` and `curry2` functions in TypeScript. We provide this example for demonstration purposes, but in real-world scenarios, manually creating curried versions for the two different argument orders is generally a better choice. 1104 | 1105 | ### Tips for creating and managing your type-level functions 1106 | 1107 | hkt-core is a _core_ library that provides essential utilities for type-level programming in TypeScript, but it doesn’t come with many built-in type-level functions out of the box. We’ve shown some examples of useful type-level functions in the previous sections, and you might create your own toolkit with a lot of useful type-level functions based on hkt-core. Below are some tips for creating and managing your type-level functions effectively. 1108 | 1109 | First, while it might seem appealing, we don’t recommend creating auto-currying type-level functions like those in [HOTScript](https://github.com/gvergnaud/HOTScript). TypeScript cannot reliably identify whether a function is partially applied or not (see [a discussion about TypeScript support in Ramda’s repository](https://github.com/ramda/ramda/issues/2976#issuecomment-706475091)), and this applies to type-level functions as well. Instead, we recommend manually creating curried functions, similar to how [Effect](https://github.com/Effect-TS/effect) and [fp-ts](https://github.com/gcanti/fp-ts) handles currying. 1110 | 1111 | Another challenge is how to manage different variants of the same type-level function — such as the simple generic version, the type-level function version, and the type-level function template version. A useful strategy is to apply different suffixes to distinguish between these variants. For example, `Map`, `Map$`, and `Map$$`, where the number of `$` indicates the number of arguments the returned type-level function accepts: 1112 | 1113 | ```typescript 1114 | type ConcatNames = Pipe< 1115 | Names, 1116 | List.Filter$>>, 1117 | List.Map$, 1118 | List.Join$<", "> 1119 | >; 1120 | 1121 | export namespace Any { 1122 | /* NotExtend */ 1123 | export type NotExtend = [T] extends [U] ? false : true; 1124 | export interface NotExtend$ extends TypeLambda<[x: unknown], boolean> { 1125 | return: NotExtend, U>; 1126 | } 1127 | export interface NotExtend$$ extends TypeLambda<[x: unknown, y: unknown], boolean> { 1128 | return: NotExtend, Arg1>; 1129 | } 1130 | } 1131 | 1132 | export namespace List { 1133 | /* Filter */ 1134 | export type Filter, TS extends unknown[]> = _Filter< 1135 | F, 1136 | TS 1137 | >; 1138 | type _Filter = 1139 | TS extends [infer Head, ...infer Tail] ? 1140 | Call1W extends true ? 1141 | _Filter 1142 | : _Filter 1143 | : Acc; 1144 | export interface Filter$> 1145 | extends TypeLambda<[xs: Param0[]], Param0[]> { 1146 | return: _Filter>; 1147 | } 1148 | export interface Filter$$ extends TypeLambdaG<["T"]> { 1149 | signature: ( 1150 | f: TypeLambda<[x: TArg], boolean>, 1151 | xs: TArg[], 1152 | ) => TArg[]; 1153 | return: _Filter, Arg1>; 1154 | } 1155 | 1156 | /* Map */ 1157 | export type Map, TS extends unknown[]> = _Map; 1158 | type _Map = { [K in keyof TS]: Call1W }; 1159 | export interface Map$ extends TypeLambda<[xs: Param0[]], RetType[]> { 1160 | return: _Map>; 1161 | } 1162 | export interface Map$$ extends TypeLambdaG<["T", "U"]> { 1163 | signature: ( 1164 | f: TypeLambda<[x: TArg], TArg>, 1165 | xs: TArg[], 1166 | ) => TArg[]; 1167 | return: _Map, Arg1>; 1168 | } 1169 | 1170 | /* Reduce */ 1171 | export type Reduce< 1172 | F extends TypeLambda2, 1173 | Init extends Param0, 1174 | TS extends unknown[], 1175 | > = _Reduce; 1176 | type _Reduce = 1177 | TS extends [infer Head, ...infer Tail] ? _Reduce> : Acc; 1178 | export interface Reduce$> 1179 | extends TypeLambda<[xs: Param1[]], Param0> { 1180 | return: _Reduce, Init>; 1181 | } 1182 | export interface Reduce$$$ extends TypeLambdaG<["T", "U"]> { 1183 | signature: ( 1184 | f: TypeLambda<[acc: TArg, x: TArg], TArg>, 1185 | init: TArg, 1186 | xs: TArg[], 1187 | ) => TArg; 1188 | return: _Reduce, Arg2, Arg1>; 1189 | } 1190 | 1191 | /* Join */ 1192 | export type Join = _Join; 1193 | type _Join = 1194 | Strings extends [infer Head extends string, ...infer Tail extends string[]] ? 1195 | _Join 1196 | : Acc; 1197 | export interface Join$ extends TypeLambda<[ss: string[]], string> { 1198 | return: _Join>; 1199 | } 1200 | export interface Join$$ extends TypeLambda<[sep: string, strings: string[]], string> { 1201 | return: _Join, Arg1>; 1202 | } 1203 | } 1204 | 1205 | export namespace Str { 1206 | /* Cap */ 1207 | export type Cap = Capitalize; 1208 | export interface Cap$ extends TypeLambda<[s: string], string> { 1209 | return: Cap>; 1210 | } 1211 | 1212 | /* Length */ 1213 | export type Length = _Length; 1214 | type _Length = 1215 | S extends `${string}${infer Tail}` ? _Length : Acc["length"]; 1216 | export interface Length$ extends TypeLambda<[s: string], number> { 1217 | return: _Length>; 1218 | } 1219 | } 1220 | ``` 1221 | 1222 | We use namespaces here to avoid polluting the global scope, and apply different suffixes to distinguish different variants of the same type-level function. For instance, `List.Map` is a simple generic type that isn’t a type-level function, `List.Map$` is a type-level function template that accepts a single argument, and `List.Map$$` is a type-level function that accepts two arguments. This naming convention allows you to easily manage various versions of the same type-level function and avoid confusion. 1223 | 1224 | ### Performance 1225 | 1226 | For experienced TypeScript developers, performance in type-level programming is a common concern. Although hkt-core uses many conditional types and recursive type aliases, it’s designed so that TypeScript simplifies types as if hkt-core weren’t even there. For example: 1227 | 1228 | ```typescript 1229 | import type { Arg0, Pipe, TypeLambda } from "hkt-core"; 1230 | 1231 | interface CapStr extends TypeLambda<[s: string], string> { 1232 | return: Capitalize>; 1233 | } 1234 | 1235 | interface StrLength extends TypeLambda<[s: string], number> { 1236 | return: _StrLength>; 1237 | } 1238 | type _StrLength = 1239 | S extends `${string}${infer Tail}` ? _StrLength : Acc["length"]; 1240 | 1241 | type F = Pipe; 1242 | ``` 1243 | 1244 | When you hover over `F`, TypeScript simplifies it to something like: 1245 | 1246 | ```typescript 1247 | type F = 1248 | Capitalize extends `${string}${infer Tail}` ? _StrLength : 0; 1249 | ``` 1250 | 1251 | This is equivalent to writing the type directly without hkt-core, demonstrating that the extra type-checking adds minimal overhead. 1252 | 1253 | However, there are cases where hkt-core can slow down the TypeScript compiler, especially when using utilities with _type validation_ (not merely _type checking_) features. In particular, `Apply` and its variants might slow down the compiler when used with _generic_ type-level functions that have complex type signatures — if you encounter this, try using `ApplyW` and `Call*W` instead. On the other hand, even though `Args` and its variants also perform type validation, they seldom affect the compiler’s performance, so normally, you can safely use them without worry. 1254 | 1255 | ## FAQ 1256 | 1257 | ### Should I add it as a dev dependency or a regular dependency? 1258 | 1259 | Even though hkt-core is a type-only library, it **should _not_** be added as a dev dependency if you are developing a library or framework that will be used by other projects. Without hkt-core as a regular dependency, the type definitions of your library will be incomplete, and users will encounter type errors when they try to use it. 1260 | 1261 | On the other hand, if you’re using hkt-core just for your own project and don’t plan to publish it as a library, you can safely add it as a dev dependency. 1262 | 1263 | ### _Generic_ type-level functions don’t infer types correctly! 1264 | 1265 | hkt-core simulates the TypeScript type system at the type level as closely as possible, but it’s not perfect. If something doesn’t work in TypeScript, it’s likely that it won’t work in hkt-core either. When encountering issues with generic type-level functions, first test whether their equivalent runtime version works in TypeScript (you can use Effect or other libraries for this). If it doesn’t work in TypeScript, it’s also unlikely to work in hkt-core. 1266 | 1267 | For example, the following code will trigger an error in TypeScript: 1268 | 1269 | ```typescript 1270 | interface Head extends TypeLambdaG<["T"]> { 1271 | signature: (tuple: [TArg, ...unknown[]]) => TArg; 1272 | return: Arg0[0]; 1273 | } 1274 | 1275 | type Composed = Flow; 1276 | // ~~~~ 1277 | // Type 'Head' does not satisfy the constraint 'TypeLambda1'. 1278 | // Types of property 'signature' are incompatible. 1279 | // Type '(tuple: [unknown, ...unknown[]]) => unknown' is not assignable to type '(args_0: unknown) => any'. 1280 | // Types of parameters 'tuple' and 'args_0' are incompatible. 1281 | // Type 'unknown' is not assignable to type '[unknown, ...unknown[]]'. 1282 | ``` 1283 | 1284 | You might expect the signature of `Composed` to be inferred as `(tuple3: [[[T, ...unknown[]], ...unknown[]], ...unknown[]]) => T`, but this is the expected behavior. If you try the equivalent runtime code in Effect, you’ll see that it still doesn’t work in TypeScript: 1285 | 1286 | ```typescript 1287 | import { flow } from "effect"; 1288 | 1289 | const head = (tuple: [T, ...unknown[]]): T => tuple[0]; 1290 | 1291 | flow(head, head, head); 1292 | // ~~~~ 1293 | // Argument of type '(tuple: [T, ...unknown[]]) => T' is not assignable to parameter of type '(tuple: [T, ...unknown[]]) => [unknown, ...unknown[]]'. 1294 | // Type 'T' is not assignable to type '[unknown, ...unknown[]]'. 1295 | ``` 1296 | 1297 | These kinds of “issues” are not considered bugs in hkt-core. In such cases, it’s often necessary to rethink your design and simplify the type-level functions to avoid complex type inference issues. 1298 | 1299 | Though many unexpected behaviors are _not_ true issues, as described above, there are also cases where unexpected behavior is due to **limitations** inherent in hkt-core, particularly with generic function composition. For example, the following code works in TypeScript because TypeScript provides special support for generic function types: 1300 | 1301 | ```typescript 1302 | declare function compose2(g: (y: U) => V, f: (x: T) => U): (x: T) => V; 1303 | 1304 | declare function toString(n: number): string; 1305 | declare function makeTuple(x: T): [T]; 1306 | 1307 | const testCompose2 = compose2(makeTuple, toString); 1308 | // ^?: (x: number) => [string] 1309 | ``` 1310 | 1311 | However, if you try to define a similar type-level `Compose2` in hkt-core, it won’t work as expected: 1312 | 1313 | ```typescript 1314 | interface Compose2 extends TypeLambdaG<["T", "U", "V"]> { 1315 | signature: ( 1316 | g: (y: TArg) => TArg, 1317 | f: (x: TArg) => TArg, 1318 | ) => (x: TArg) => TArg; 1319 | } 1320 | 1321 | declare function toString(n: number): string; 1322 | declare function makeTuple(x: T): [T]; 1323 | 1324 | type TestCompose2 = RetType; // => (x: number) => [unknown] 1325 | ``` 1326 | 1327 | Here, the return type is inferred as `(x: number) => [unknown]` instead of `(x: number) => [string]`. Since hkt-core simulates the TypeScript type system at the type level, it cannot provide the same special support for generic function types that TypeScript does. 1328 | 1329 | While these limitations are not expected to be fixed in the near future, you’re welcome to open an issue to present these cases and discuss potential solutions. 1330 | 1331 | If you believe you’ve encountered a bug — where the equivalent runtime code works in TypeScript but hkt-core behaves unexpectedly — please [open an issue on the GitHub repository](https://www.github.com/Snowflyt/hkt-core/issues) with a minimal reproduction case. Some known issues are already listed there, so please check whether your issue has already been reported before submitting a new one. 1332 | 1333 | ### What’s the magic behind generic type-level functions? 1334 | 1335 | Here’s a minimal example of a generic type-level function: 1336 | 1337 | ```typescript 1338 | type TODO = any; 1339 | 1340 | // Utility types for type arguments 1341 | type ArgT = F extends { T: infer T } ? T : never; 1342 | type ArgU = F extends { U: infer U } ? U : never; 1343 | 1344 | // Define a generic type-level function 1345 | interface Map { 1346 | signature: (f: (x: ArgT) => ArgU, xs: ArgT[]) => ArgU[]; 1347 | return: TODO; 1348 | } 1349 | 1350 | // Instantiate type arguments from known parameter types 1351 | type TypeArgs = 1352 | // ^?: { T: string; U: number } 1353 | ((f: (x: string) => number, xs: string[]) => never) extends ( 1354 | (Map & { T: infer T; U: infer U })["signature"] 1355 | ) ? 1356 | { T: T; U: U } 1357 | : never; 1358 | 1359 | // Get the refined signature with instantiated type args 1360 | type _ = (Map & TypeArgs)["signature"]; 1361 | // ^?: (f: (x: string) => number, xs: string[]) => number[] 1362 | ``` 1363 | 1364 | We use `ArgT` and `ArgU` to extract the type arguments from the type-level function, just like `TArg` in hkt-core. We then instantiate the type arguments from known parameter types and refine the signature with the instantiated type arguments. This is the basic idea behind generic type-level functions. 1365 | 1366 | We use `ArgT` and `ArgU` to extract the type arguments from the type-level function — just like `TArg` in hkt-core. Then we instantiate these type arguments from known parameter types and refine the signature accordingly. This is the core idea behind generic type-level functions. 1367 | 1368 | While the example above works well for simple cases, it doesn’t handle subtyping correctly. For instance, the following code will simply yield never: 1369 | 1370 | ```typescript 1371 | // We replace `xs: string[]` with `xs: "foo"[]` to test subtyping 1372 | type TypeArgs = 1373 | // ^?: never 1374 | ((f: (x: string) => number, xs: "foo"[]) => never) extends ( 1375 | (Map & { T: infer T; U: infer U })["signature"] 1376 | ) ? 1377 | { T: T; U: U } 1378 | : never; 1379 | ``` 1380 | 1381 | To support subtyping properly, we need to **flip the variance** of parameter types in our type-level function. We achieve this by wrapping each parameter with a helper type that inverts the variance: 1382 | 1383 | ```typescript 1384 | // Flip the variance of a type 1385 | interface In { 1386 | (_: T): void; 1387 | } 1388 | 1389 | // Same as before 1390 | type ArgT = F extends { T: infer T } ? T : never; 1391 | type ArgU = F extends { U: infer U } ? U : never; 1392 | 1393 | // Wrap each parameter with `In<...>` 1394 | interface Map { 1395 | signature: (f: In<(x: ArgT) => ArgU>, xs: In[]>) => ArgU[]; 1396 | return: any; 1397 | } 1398 | 1399 | // Still wrap each parameter with `In<...>`, and... 1400 | // Now it works! 1401 | type TypeArgs = 1402 | // ^?: { T: "foo"; U: number } 1403 | ((f: In<(x: string) => number>, xs: In<"foo"[]>) => never) extends ( 1404 | (Map & { T: infer T; U: infer U })["signature"] 1405 | ) ? 1406 | { T: T; U: U } 1407 | : never; 1408 | 1409 | type _ = (Map & TypeArgs)["signature"]; 1410 | // ^?: (f: In<(x: "foo") => number>, xs: In<"foo"[]>) => number[ 1411 | ``` 1412 | 1413 | This might seem like magic at first, and it can be a bit tricky to grasp initially. We’ll skip the finer details here, but if you’re curious, you can apply the variance rules in TypeScript step by step to see how everything falls into place. 1414 | 1415 | Internally, hkt-core uses a similar technique to handle variance correctly, so you don’t have to write this kind of code yourself. Understanding the basic idea behind generic type-level functions can help explain why some seemingly simple functions might not infer types correctly in edge cases. 1416 | 1417 | ### Why not just access arguments and type parameters via `this`? 1418 | 1419 | Libraries like [HOTScript](https://github.com/gvergnaud/hotscript) use syntax like `this["arg0"]` to access arguments inside a type-level function. While this approach seems simpler and more concise, it introduces unnecessary properties to the type-level function, which can lead to variance issues. Let’s first revisit the variance of function types in TypeScript: 1420 | 1421 | ```typescript 1422 | type IsSubtype = [T] extends [U] ? true : false; 1423 | 1424 | // All types are subtypes of `unknown` 1425 | type Test1 = IsSubtype; // => true 1426 | // `never` is the subtype of all types 1427 | type Test2 = IsSubtype; // => true 1428 | 1429 | // `() => string` is also a subtype of `() => unknown`, 1430 | // indicating the return type of a function is covariant 1431 | type TestReturn = IsSubtype<() => string, () => unknown>; // => true 1432 | // While `(_: string) => void` is not a subtype of `(_: unknown) => void`, 1433 | // instead, `(_: unknown) => void` is a subtype of `(_: string) => void`, 1434 | // indicating the parameter type of a function is contravariant 1435 | type TestParam1 = IsSubtype<(_: string) => void, (_: unknown) => void>; // => false 1436 | type TestParam2 = IsSubtype<(_: unknown) => void, (_: string) => void>; // => true 1437 | 1438 | // So we get the following result 1439 | type TestFunc = IsSubtype<(_: string) => number, (_: never) => unknown>; // => true 1440 | ``` 1441 | 1442 | hkt-core provides a _typed_ version of type-level functions, so we should keep the variance rules of `TypeLambda` aligned with TypeScript’s function types. However, if we add extra properties like `args` to `TypeLambda` for convenience to access arguments, we can break the variance rules: 1443 | 1444 | ```typescript 1445 | interface TypeLambda { 1446 | signature: (...args: Params) => R; 1447 | args: Params; 1448 | } 1449 | 1450 | // Oops! This _should_ be `true` as expected 1451 | type TestVariance = IsSubtype, TypeLambda<[never], unknown>>; // => false 1452 | ``` 1453 | 1454 | If we remove the `args: Params` from the code above, we get the standard `TypeLambda` implementation as it is defined in `hkt-core`, and the result of `TestVariance` will be `true` as expected. However, by adding `args: Params` back, `Params` now appears in both a contravariant position (as function parameters in the `signature` property) and a covariant position (as the `args` property). This makes `Params` invariant and breaks the variance rules. 1455 | 1456 | Now, you might wonder, what if we use a contravariant **`args`** property, like `args: (_: Params) => void`? While this would work, it forces the user to access the arguments using the more complex syntax `Parameters[0]`, which doesn’t provide much benefit over the standard **`Args`** utility in **hkt-core**. 1457 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {object} Parsed 5 | * @property {?string} emoji The emoji at the beginning of the commit message. 6 | * @property {?string} type The type of the commit message. 7 | * @property {?string} scope The scope of the commit message. 8 | * @property {?string} subject The subject of the commit message. 9 | */ 10 | 11 | const emojiEnum = /** @type {const} */ ([ 12 | 2, 13 | "always", 14 | { 15 | "🎉": ["init", "Project initialization"], 16 | "✨": ["feat", "Adding new features"], 17 | "🐞": ["fix", "Fixing bugs"], 18 | "📃": ["docs", "Modify documentation only"], 19 | "🌈": [ 20 | "style", 21 | "Only the spaces, formatting indentation, commas, etc. were changed, not the code logic", 22 | ], 23 | "🦄": ["refactor", "Code refactoring, no new features added or bugs fixed"], 24 | "🎈": ["perf", "Optimization-related, such as improving performance, experience"], 25 | "🧪": ["test", "Adding or modifying test cases"], 26 | "🔧": [ 27 | "build", 28 | "Dependency-related content, such as Webpack, Vite, Rollup, npm, package.json, etc.", 29 | ], 30 | "🐎": ["ci", "CI configuration related, e.g. changes to k8s, docker configuration files"], 31 | "🐳": ["chore", "Other modifications, e.g. modify the configuration file"], 32 | "↩": ["revert", "Rollback to previous version"], 33 | }, 34 | ]); 35 | 36 | /** @satisfies {import('@commitlint/types').UserConfig} */ 37 | const config = { 38 | parserPreset: { 39 | parserOpts: { 40 | headerPattern: 41 | /^(?\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]) (?\w+)(?:\((?.*)\))?!?: (?(?:(?!#).)*(?:(?!\s).))$/, 42 | headerCorrespondence: ["emoji", "type", "scope", "subject"], 43 | }, 44 | }, 45 | plugins: [ 46 | { 47 | rules: { 48 | "header-match-git-commit-message-with-emoji-pattern": (parsed) => { 49 | const { emoji, scope, subject, type } = /** @type {Parsed} */ ( 50 | /** @type {unknown} */ (parsed) 51 | ); 52 | if (emoji === null && type === null && scope === null && subject === null) 53 | return [ 54 | false, 55 | 'header must be in format " (?): ", e.g:\n' + 56 | " - 🎉 init: Initial commit\n" + 57 | " - ✨ feat(assertions): Add assertions\n" + 58 | " ", 59 | ]; 60 | return [true, ""]; 61 | }, 62 | "emoji-enum": (parsed, _, value) => { 63 | const { emoji } = /** @type {Parsed} */ (/** @type {unknown} */ (parsed)); 64 | const emojisObject = /** @type {typeof emojiEnum[2]} */ (/** @type {unknown} */ (value)); 65 | if (emoji && !Object.keys(emojisObject).includes(emoji)) { 66 | return [ 67 | false, 68 | "emoji must be one of:\n" + 69 | Object.entries(emojisObject) 70 | .map(([emoji, [type, description]]) => ` ${emoji} ${type} - ${description}`) 71 | .join("\n") + 72 | "\n ", 73 | ]; 74 | } 75 | return [true, ""]; 76 | }, 77 | }, 78 | }, 79 | ], 80 | rules: { 81 | "header-match-git-commit-message-with-emoji-pattern": [2, "always"], 82 | "body-leading-blank": [2, "always"], 83 | "footer-leading-blank": [2, "always"], 84 | "header-max-length": [2, "always", 72], 85 | "scope-case": [2, "always", ["lower-case", "upper-case"]], 86 | "subject-case": [2, "always", "sentence-case"], 87 | "subject-empty": [2, "never"], 88 | "subject-exclamation-mark": [2, "never"], 89 | "subject-full-stop": [2, "never", "."], 90 | "emoji-enum": emojiEnum, 91 | "type-case": [2, "always", "lower-case"], 92 | "type-empty": [2, "never"], 93 | "type-enum": [ 94 | 2, 95 | "always", 96 | [ 97 | "init", 98 | "feat", 99 | "fix", 100 | "docs", 101 | "style", 102 | "refactor", 103 | "perf", 104 | "test", 105 | "build", 106 | "ci", 107 | "chore", 108 | "revert", 109 | ], 110 | ], 111 | }, 112 | }; 113 | 114 | export default config; 115 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from "@eslint/js"; 4 | import importX from "eslint-plugin-import-x"; 5 | import jsdoc from "eslint-plugin-jsdoc"; 6 | import prettierRecommended from "eslint-plugin-prettier/recommended"; 7 | import sonarjs from "eslint-plugin-sonarjs"; 8 | import sortDestructureKeys from "eslint-plugin-sort-destructure-keys"; 9 | import globals from "globals"; 10 | import tseslint from "typescript-eslint"; 11 | 12 | export default tseslint.config( 13 | eslint.configs.recommended, 14 | tseslint.configs.strictTypeChecked, 15 | tseslint.configs.stylisticTypeChecked, 16 | jsdoc.configs["flat/recommended-typescript-error"], 17 | importX.flatConfigs.recommended, 18 | importX.flatConfigs.typescript, 19 | prettierRecommended, 20 | sonarjs.configs.recommended, 21 | { 22 | plugins: { 23 | jsdoc, 24 | "sort-destructure-keys": sortDestructureKeys, 25 | }, 26 | linterOptions: { 27 | reportUnusedDisableDirectives: true, 28 | }, 29 | languageOptions: { 30 | parserOptions: { 31 | projectService: { 32 | allowDefaultProject: ["*.{js,cjs}", "typroof.config.ts"], 33 | defaultProject: "tsconfig.json", 34 | }, 35 | tsconfigRootDir: import.meta.dirname, 36 | }, 37 | globals: { ...globals.browser }, 38 | }, 39 | rules: { 40 | "@typescript-eslint/restrict-plus-operands": [ 41 | "error", 42 | { allowAny: true, allowNumberAndString: true }, 43 | ], 44 | "@typescript-eslint/restrict-template-expressions": [ 45 | "error", 46 | { allowAny: true, allowBoolean: true, allowNullish: true, allowNumber: true }, 47 | ], 48 | "@typescript-eslint/consistent-indexed-object-style": "off", 49 | "@typescript-eslint/consistent-type-definitions": "off", // TS treats types and interfaces differently, this may break some advanced type gymnastics 50 | "@typescript-eslint/consistent-type-imports": [ 51 | "error", 52 | { prefer: "type-imports", disallowTypeAnnotations: false }, 53 | ], 54 | "@typescript-eslint/dot-notation": ["error", { allowIndexSignaturePropertyAccess: true }], 55 | "@typescript-eslint/no-confusing-void-expression": "off", 56 | "@typescript-eslint/no-empty-function": "off", 57 | "@typescript-eslint/no-empty-object-type": "off", 58 | "@typescript-eslint/no-explicit-any": "off", 59 | "@typescript-eslint/no-extraneous-class": "off", 60 | "@typescript-eslint/no-invalid-void-type": "off", 61 | "@typescript-eslint/no-namespace": "off", 62 | "@typescript-eslint/no-non-null-assertion": "off", 63 | "@typescript-eslint/no-unnecessary-type-parameters": "off", 64 | "@typescript-eslint/no-unsafe-argument": "off", 65 | "@typescript-eslint/no-unsafe-assignment": "off", 66 | "@typescript-eslint/no-unsafe-call": "off", 67 | "@typescript-eslint/no-unsafe-member-access": "off", 68 | "@typescript-eslint/no-unsafe-return": "off", 69 | "@typescript-eslint/no-unused-vars": "off", // Already covered by `tsconfig.json` 70 | "@typescript-eslint/prefer-nullish-coalescing": "off", 71 | "@typescript-eslint/prefer-optional-chain": "off", // This library targets ES2015, so optional chaining is not available 72 | "@typescript-eslint/unified-signatures": "off", 73 | "import-x/consistent-type-specifier-style": ["error", "prefer-top-level"], 74 | "import-x/no-named-as-default-member": "off", 75 | "import-x/no-unresolved": "off", 76 | "import-x/order": [ 77 | "error", 78 | { 79 | alphabetize: { order: "asc" }, 80 | groups: ["builtin", "external", "internal", "parent", "sibling", "index", "object"], 81 | "newlines-between": "always", 82 | }, 83 | ], 84 | "jsdoc/check-param-names": "off", 85 | "jsdoc/check-tag-names": "off", 86 | "jsdoc/check-values": "off", 87 | "jsdoc/no-types": "off", // Already checked by TypeScript 88 | "jsdoc/require-jsdoc": "off", 89 | "jsdoc/require-param": "off", 90 | "jsdoc/require-returns-description": "off", 91 | "jsdoc/tag-lines": "off", 92 | "no-restricted-syntax": [ 93 | "error", 94 | { 95 | selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments", 96 | message: 97 | "Do not use spread arguments in `Array#push`, " + 98 | "as it might cause stack overflow if you spread a large array. " + 99 | "Instead, use `Array#concat` or `Array.prototype.push.apply`.", 100 | }, 101 | ], 102 | "no-undef": "off", // Already checked by TypeScript 103 | "object-shorthand": "error", 104 | "sonarjs/class-name": ["error", { format: "^_*[A-Z][a-zA-Z0-9]*$" }], 105 | "sonarjs/code-eval": "off", // Already covered by `@typescript-eslint/no-implied-eval` 106 | "sonarjs/cognitive-complexity": "off", 107 | "sonarjs/deprecation": "off", // Already covered by `@typescript-eslint/no-deprecated` 108 | "sonarjs/different-types-comparison": "off", // Already checked by TypeScript 109 | "sonarjs/function-return-type": "off", // Already checked by TypeScript 110 | "sonarjs/generator-without-yield": "off", // Already covered by `require-yield` 111 | "sonarjs/no-alphabetical-sort": "off", 112 | "sonarjs/no-control-regex": "off", // Already covered by `no-control-regex` 113 | "sonarjs/no-dead-store": "off", // Already checked by TypeScript 114 | "sonarjs/no-empty-test-file": "off", 115 | "sonarjs/no-ignored-exceptions": "off", 116 | "sonarjs/no-nested-assignment": "off", 117 | "sonarjs/no-nested-conditional": "off", 118 | "sonarjs/no-nested-functions": "off", 119 | "sonarjs/no-primitive-wrappers": "off", // Already covered by `@typescript-eslint/no-wrapper-object-types` 120 | "sonarjs/no-selector-parameter": "off", 121 | "sonarjs/no-useless-intersection": "off", // Already checked by TypeScript 122 | "sonarjs/no-unused-vars": "off", // Already checked by TypeScript 123 | "sonarjs/reduce-initial-value": "off", 124 | "sonarjs/redundant-type-aliases": "off", // Already covered by `@typescript-eslint/no-restricted-type-imports` 125 | "sonarjs/regex-complexity": "off", 126 | "sonarjs/todo-tag": "off", 127 | "sonarjs/void-use": "off", 128 | "sort-destructure-keys/sort-destructure-keys": "error", 129 | "sort-imports": ["error", { ignoreDeclarationSort: true }], 130 | }, 131 | }, 132 | { 133 | files: ["**/*.test.ts"], 134 | rules: { 135 | "@typescript-eslint/ban-ts-comment": "off", 136 | }, 137 | }, 138 | { 139 | files: ["typroof-plugin/**/*.ts"], 140 | rules: { 141 | "@typescript-eslint/only-throw-error": "off", 142 | }, 143 | }, 144 | ); 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hkt-core", 3 | "version": "1.1.0", 4 | "private": true, 5 | "description": "🍃 A micro HKT (higher-kinded type) implementation for TypeScript, with type safety elegantly guaranteed.", 6 | "keywords": [ 7 | "type gymnastics", 8 | "type level", 9 | "type safe", 10 | "generic", 11 | "generic programming", 12 | "hkt", 13 | "higher kinded type", 14 | "type level programming" 15 | ], 16 | "homepage": "https://github.com/Snowflyt/hkt-core", 17 | "bugs": { 18 | "url": "https://github.com/Snowflyt/hkt-core/issues" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/Snowflyt/hkt-core" 23 | }, 24 | "license": "MIT", 25 | "author": "Ge Gao (Snowflyt) ", 26 | "type": "module", 27 | "types": "./index.d.ts", 28 | "workspaces": [ 29 | "typroof-plugin" 30 | ], 31 | "scripts": { 32 | "prebuild": "npm run clean", 33 | "build": "tsc --noEmit && cpy \"src/**/*\" dist && rimraf --glob \"dist/**/*.test.ts\" && renamer --find .ts --replace .d.ts \"dist/**\" && replace-in-file \"/^\\s*\\/\\/ eslint-disable-next-line .+$/mg\" \"\" \"dist/**/*.d.ts\" --isRegex && replace-in-file \"/^\\s*\\/\\*\\s+eslint-disable\\s+(\\S+\\s+)?\\*\\/$/mg\" \"\" \"dist/**/*.d.ts\" --isRegex && prettier --log-level=silent --write \"dist/**/*\" --ignore-path !dist/ && cpy package.json dist && json -I -f dist/package.json -e \"delete this.private; delete this.workspaces; delete this.scripts; delete this.devDependencies\" && cpy LICENSE dist && cpy README.md dist", 34 | "clean": "rimraf dist", 35 | "format": "prettier --no-error-on-unmatched-pattern --write **/*.{js,ts,json,md} *.{cjs,mjs,cts,mts}", 36 | "lint": "eslint **/*.{js,ts} *.{cjs,mjs,cts,mts} --no-error-on-unmatched-pattern --report-unused-disable-directives-severity error --max-warnings 0", 37 | "lint:fix": "eslint --fix **/*.{js,ts} *.{cjs,mjs,cts,mts} --no-error-on-unmatched-pattern --report-unused-disable-directives-severity error --max-warnings 0", 38 | "prepare": "node -e \"import fs from 'node:fs'; import path from 'node:path'; const hooksDir = path.join(process.cwd(), '.githooks'); const gitHooksDir = path.join(process.cwd(), '.git/hooks'); if (!fs.existsSync(gitHooksDir)) { console.error('Git hooks directory not found, please run this in a git repository.'); process.exit(1); } fs.readdirSync(hooksDir).forEach(file => { const srcFile = path.join(hooksDir, file); const destFile = path.join(gitHooksDir, file); fs.copyFileSync(srcFile, destFile); if (process.platform !== 'win32' && !file.endsWith('.cmd')) { fs.chmodSync(destFile, 0o755); } })\"", 39 | "test": "tsc --noEmit && typroof" 40 | }, 41 | "devDependencies": { 42 | "@typescript-eslint/parser": "^8.31.0", 43 | "commitlint": "^19.8.0", 44 | "cpy-cli": "^5.0.0", 45 | "effect": "^3.14.14", 46 | "eslint": "^9.25.1", 47 | "eslint-config-prettier": "^10.1.2", 48 | "eslint-import-resolver-typescript": "^4.3.4", 49 | "eslint-plugin-import-x": "^4.11.0", 50 | "eslint-plugin-jsdoc": "^50.6.11", 51 | "eslint-plugin-prettier": "^5.2.6", 52 | "eslint-plugin-sonarjs": "^3.0.2", 53 | "eslint-plugin-sort-destructure-keys": "^2.0.0", 54 | "globals": "^16.0.0", 55 | "json": "^11.0.0", 56 | "prettier": "^3.5.3", 57 | "prettier-plugin-packagejson": "^2.5.10", 58 | "renamer": "^5.0.2", 59 | "replace-in-file": "^8.3.0", 60 | "rimraf": "^6.0.1", 61 | "typescript": "latest", 62 | "typescript-eslint": "^8.31.0", 63 | "typroof": "^0.5.1" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = /** @satisfies {import("prettier").Config} */ ({ 4 | arrowParens: "always", 5 | bracketSameLine: true, 6 | bracketSpacing: true, 7 | experimentalTernaries: true, 8 | plugins: ["prettier-plugin-packagejson"], 9 | semi: true, 10 | singleQuote: false, 11 | trailingComma: "all", 12 | printWidth: 100, 13 | }); 14 | -------------------------------------------------------------------------------- /test/Always.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import { beOfSig } from "@hkt-core/typroof-plugin"; 4 | 5 | import type { Always, Apply } from "../src"; 6 | 7 | test("Always", () => { 8 | type Always42 = Always<42>; 9 | 10 | expect().to(beOfSig<() => 42>); 11 | expect>().to(equal<42>); 12 | }); 13 | -------------------------------------------------------------------------------- /test/Apply.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import type { Apply, Arg0, Arg1, TypeLambda } from "../src"; 4 | 5 | test("Apply", () => { 6 | interface ParseNumber extends TypeLambda<[n: string], number> { 7 | return: Arg0 extends `${infer N extends number}` ? N : never; 8 | } 9 | expect>().to(equal<42>); 10 | 11 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 12 | return: `${Arg0}${Arg1}`; 13 | } 14 | expect>().to(equal<"foobar">); 15 | }); 16 | -------------------------------------------------------------------------------- /test/Args.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, equal, expect } from "typroof"; 2 | 3 | import type { ApplyW, Args, TypeLambda } from "../src"; 4 | 5 | describe("Args", () => { 6 | interface PrintArgs extends TypeLambda<[a: string, b: string], string> { 7 | return: Args; 8 | } 9 | 10 | // Incompatible arguments are cast to `never` 11 | expect>().to(equal<["foo", never]>); 12 | // Redundant arguments are truncated 13 | expect>().to(equal<["foo", "bar"]>); 14 | // Missing arguments are filled with `never` 15 | expect>().to(equal<["foo", never]>); 16 | }); 17 | -------------------------------------------------------------------------------- /test/Args.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, equal, expect, it } from "typroof"; 2 | 3 | import type { ApplyW, Args, TypeLambda } from "../src"; 4 | 5 | describe("Args", () => { 6 | interface PrintArgs extends TypeLambda<[a: string, b: string], string> { 7 | return: Args; 8 | } 9 | 10 | it("should cast incompatible arguments to `never`", () => { 11 | expect>().to(equal<["foo", never]>); 12 | expect>().to(equal<[never, "foo"]>); 13 | }); 14 | 15 | it("should truncate redundant arguments", () => { 16 | expect>().to(equal<["foo", "bar"]>); 17 | }); 18 | 19 | it("should fill missing arguments with `never`", () => { 20 | expect>().to(equal<["foo", never]>); 21 | }); 22 | 23 | it("should follow all three rules at the same time", () => { 24 | expect>().to(equal<[never, "foo"]>); 25 | expect>().to(equal<[never, never]>); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/Ask.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import { beOfSig } from "@hkt-core/typroof-plugin"; 4 | 5 | import type { Apply, Ask, Flow, Identity } from "../src"; 6 | 7 | test("Ask", () => { 8 | type AskNumber = Ask; 9 | expect().to(beOfSig<(value: number) => number>); 10 | expect>().to(equal<42>); 11 | 12 | // Fix the input type of `Identity` to `string` 13 | type AskString = Flow, Identity>; 14 | expect().to(beOfSig<(value: string) => string>); 15 | expect>().to(equal<"foo">); 16 | }); 17 | -------------------------------------------------------------------------------- /test/Curry.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, equal, expect, it } from "typroof"; 2 | 3 | import { beOfSig } from "@hkt-core/typroof-plugin"; 4 | 5 | import type { 6 | Apply, 7 | Arg0, 8 | Arg1, 9 | Arg2, 10 | Call1W, 11 | Call2W, 12 | Curry, 13 | TArg, 14 | TypeLambda, 15 | TypeLambdaG, 16 | } from "../src"; 17 | 18 | describe("Curry", () => { 19 | it("should curry a binary type-level function", () => { 20 | interface Append extends TypeLambda<[s: string], string> { 21 | return: `${Arg0}${Suffix}`; 22 | } 23 | 24 | interface Map extends TypeLambdaG<["T", "U"]> { 25 | signature: ( 26 | f: TypeLambda<[x: TArg], TArg>, 27 | xs: TArg[], 28 | ) => TArg[]; 29 | return: _Map, Arg1>; 30 | } 31 | type _Map = { [K in keyof TS]: Call1W }; 32 | 33 | expect().to(beOfSig<(f: (x: T) => U, xs: T[]) => U[]>); 34 | expect, ["foo", "bar"]]>>().to(equal<["foo!", "bar!"]>); 35 | 36 | type CurriedMap = Curry; 37 | expect().to(beOfSig<(f: (x: T) => U) => (xs: T[]) => U[]>); 38 | expect]>>().to(beOfSig<(xs: string[]) => string[]>); 39 | expect]>, [["foo", "bar"]]>>().to(equal<["foo!", "bar!"]>); 40 | }); 41 | 42 | it("should curry a ternary type-level function", () => { 43 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 44 | return: `${Arg0}${Arg1}`; 45 | } 46 | 47 | interface Reduce extends TypeLambdaG<["T", "U"]> { 48 | signature: ( 49 | f: TypeLambda<[acc: TArg, x: TArg], TArg>, 50 | init: TArg, 51 | xs: TArg[], 52 | ) => TArg; 53 | return: _Reduce, Arg1, Arg2>; 54 | } 55 | type _Reduce = 56 | TS extends readonly [infer Head, ...infer Tail] ? _Reduce, Tail> 57 | : Acc; 58 | 59 | expect().to(beOfSig<(f: (acc: U, x: T) => U, init: U, xs: T[]) => U>); 60 | expect>().to(equal<"foobarbaz">); 61 | 62 | type CurriedReduce = Curry; 63 | expect().to( 64 | beOfSig<(f: (acc: U, x: T) => U) => (init: U) => (xs: T[]) => U>, 65 | ); 66 | expect>().to( 67 | beOfSig<(init: string) => (xs: string[]) => string>, 68 | ); 69 | expect, [""]>>().to(beOfSig<(xs: string[]) => string>); 70 | expect, [""]>, [["foo", "bar", "baz"]]>>().to( 71 | equal<"foobarbaz">, 72 | ); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/Flip.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, equal, expect, it } from "typroof"; 2 | 3 | import { beOfSig } from "@hkt-core/typroof-plugin"; 4 | 5 | import type { Apply, Arg0, Arg1, Call1W, Curry, Flip, TArg, TypeLambda, TypeLambdaG } from "../src"; 6 | 7 | describe("Flip", () => { 8 | interface Append extends TypeLambda<[s: string], string> { 9 | return: `${Arg0}${Suffix}`; 10 | } 11 | 12 | interface Map extends TypeLambdaG<["T", "U"]> { 13 | signature: ( 14 | f: TypeLambda<[x: TArg], TArg>, 15 | xs: TArg[], 16 | ) => TArg[]; 17 | return: _Map, Arg1>; 18 | } 19 | type _Map = { [K in keyof TS]: Call1W }; 20 | 21 | it("should flip the arguments of a binary type-level function", () => { 22 | expect().to(beOfSig<(f: (x: T) => U, xs: T[]) => U[]>); 23 | expect, ["foo", "bar"]]>>().to(equal<["foo!", "bar!"]>); 24 | 25 | type FlippedMap = Flip; 26 | expect().to(beOfSig<(xs: T[], f: (x: T) => U) => U[]>); 27 | expect]>>().to(equal<["foo!", "bar!"]>); 28 | }); 29 | 30 | it("should flip the arguments of a curried binary type-level function", () => { 31 | type CurriedMap = Curry; 32 | 33 | expect().to(beOfSig<(f: (x: T) => U) => (xs: T[]) => U[]>); 34 | expect]>>().to(beOfSig<(xs: string[]) => string[]>); 35 | expect]>, [["foo", "bar"]]>>().to(equal<["foo!", "bar!"]>); 36 | 37 | type FlippedCurriedMap = Flip; 38 | 39 | expect().to(beOfSig<(xs: T[]) => (f: (x: T) => U) => U[]>); 40 | expect>().to( 41 | beOfSig<(f: (x: string) => unknown) => unknown[]>, 42 | ); 43 | expect, [Append<"!">]>>().to( 44 | equal<["foo!", "bar!"]>, 45 | ); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/Identity.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import type { Apply, Identity } from "../src"; 4 | 5 | test("Identity", () => { 6 | expect>().to(equal<42>); 7 | }); 8 | -------------------------------------------------------------------------------- /test/Params.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import type { Arg0, Arg1, Params, TypeLambda } from "../src"; 4 | 5 | test("Params", () => { 6 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 7 | return: `${Arg0}${Arg1}`; 8 | } 9 | 10 | expect>().to(equal<[s1: string, s2: string]>); 11 | }); 12 | -------------------------------------------------------------------------------- /test/ParamsLength.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import type { Arg0, Arg1, ParamsLength, TypeLambda } from "../src"; 4 | 5 | test("ParamsLength", () => { 6 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 7 | return: `${Arg0}${Arg1}`; 8 | } 9 | 10 | expect>().to(equal<2>); 11 | }); 12 | -------------------------------------------------------------------------------- /test/README.test.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from "effect"; 2 | import { filter, map } from "effect/Array"; 3 | import { beNever, equal, error, expect, test } from "typroof"; 4 | 5 | import { beOfSig } from "@hkt-core/typroof-plugin"; 6 | 7 | import type { 8 | Always, 9 | Apply, 10 | ApplyW, 11 | Arg0, 12 | Arg1, 13 | Arg2, 14 | Args, 15 | Ask, 16 | Call1, 17 | Call1W, 18 | Call2, 19 | Call2W, 20 | Call3, 21 | Compose, 22 | Curry, 23 | Flip, 24 | Flow, 25 | HKT, 26 | Identity, 27 | Kind, 28 | Param0, 29 | Params, 30 | Pipe, 31 | RawArg0, 32 | RawArg1, 33 | RetType, 34 | Sig, 35 | TArg, 36 | TolerantParams, 37 | TolerantRetType, 38 | Tupled, 39 | TypeArgs, 40 | TypeLambda, 41 | TypeLambda0, 42 | TypeLambda1, 43 | TypeLambdaG, 44 | Untupled, 45 | } from "../src"; 46 | 47 | test("Quickstart > Use as classical HKTs 🐱", () => { 48 | type Option = { _tag: "Some"; value: T } | { _tag: "None" }; 49 | const some = (value: T): Option => ({ _tag: "Some", value }); 50 | const none: Option = { _tag: "None" }; 51 | 52 | { 53 | const arrayMonad = { 54 | of: (a: T) => [a], 55 | flatMap: (fa: T[], f: (a: T) => U[]) => fa.flatMap(f), 56 | }; 57 | 58 | const optionMonad = { 59 | of: some, 60 | flatMap: (fa: Option, f: (a: T) => Option) => 61 | fa._tag === "Some" ? f(fa.value) : none, 62 | }; 63 | 64 | const flattenArray = (ffa: T[][]): T[] => arrayMonad.flatMap(ffa, (x) => x); 65 | const flattenOption = (ffa: Option>): Option => 66 | optionMonad.flatMap(ffa, (x) => x); 67 | } 68 | 69 | { 70 | interface MonadTypeClass { 71 | of: (a: T) => Kind; 72 | flatMap: (fa: Kind, f: (a: T) => Kind) => Kind; 73 | } 74 | 75 | const createFlatten = 76 | (monad: MonadTypeClass) => 77 | (ffa: Kind>): Kind => 78 | monad.flatMap(ffa, (x) => x); 79 | 80 | interface ArrayHKT extends HKT { 81 | return: Arg0[]; 82 | } 83 | const arrayMonad: MonadTypeClass = { 84 | of: (a) => [a], 85 | flatMap: (fa, f) => fa.flatMap(f), 86 | }; 87 | 88 | interface OptionHKT extends HKT { 89 | return: Option>; 90 | } 91 | const optionMonad: MonadTypeClass = { 92 | of: some, 93 | flatMap: (fa, f) => (fa._tag === "Some" ? f(fa.value) : none), 94 | }; 95 | 96 | expect(createFlatten(arrayMonad)).to(equal<(ffa: T[][]) => T[]>); 97 | expect(createFlatten(optionMonad)).to(equal<(ffa: Option>) => Option>); 98 | } 99 | }); 100 | 101 | test("Quickstart > Use as type-level functions ✨", () => { 102 | const capitalize = (s: string) => (s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : ""); 103 | 104 | { 105 | const concatNames = (names: string[]) => 106 | names 107 | .filter((name) => name.length > 2) 108 | .map(capitalize) 109 | .join(", "); 110 | 111 | expect(concatNames).to(equal<(names: string[]) => string>); 112 | } 113 | 114 | { 115 | const joinBy = (sep: string) => (strings: string[]) => strings.join(sep); 116 | 117 | const concatNames = (names: string[]) => 118 | pipe( 119 | names, 120 | filter((name) => name.length > 2), 121 | map(capitalize), 122 | joinBy(", "), 123 | ); 124 | 125 | expect(concatNames).to(equal<(names: string[]) => string>); 126 | } 127 | 128 | type Names = ["alice", "bob", "i", "charlie"]; 129 | 130 | { 131 | type FilterOutShortNames = 132 | Names extends [infer Head extends string, ...infer Tail extends string[]] ? 133 | Head extends `${infer A}${infer B}${infer C}` ? 134 | "" extends A | B | C ? 135 | FilterOutShortNames 136 | : [Head, ...FilterOutShortNames] 137 | : FilterOutShortNames 138 | : []; 139 | 140 | type CapitalizeNames = { 141 | [K in keyof Names]: Capitalize; 142 | }; 143 | 144 | type JoinNames = 145 | Names extends [infer Head extends string, ...infer Tail extends string[]] ? 146 | Tail extends [] ? 147 | Head 148 | : `${Head}, ${JoinNames}` 149 | : ""; 150 | 151 | type ConcatNames = JoinNames< 152 | CapitalizeNames> 153 | >; 154 | 155 | expect>().to(equal<"Alice, Bob, Charlie">); 156 | } 157 | 158 | { 159 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 160 | return: `${Arg0}${Arg1}`; 161 | } 162 | 163 | expect().to(beOfSig<(s1: string, s2: string) => string>); 164 | 165 | expect>().to(equal<"HelloWorld">); 166 | expect>().to(equal<"foobar">); 167 | 168 | // @ts-expect-error 169 | expect>().to(error); 170 | // @ts-expect-error 171 | expect>().to(error); 172 | } 173 | 174 | { 175 | interface ConcatFoo extends TypeLambda<[s: string], string> { 176 | return: `${Arg0}foo`; 177 | } 178 | interface ConcatBar extends TypeLambda<[s: string], string> { 179 | return: `${Arg0}bar`; 180 | } 181 | 182 | type Composed = Flow; 183 | expect().to(beOfSig<(s: string) => string>); 184 | expect>().to(equal<"hellofoobar">); 185 | 186 | type ConcatFooBar = Pipe; 187 | expect>().to(equal<"hellofoobar">); 188 | 189 | interface Add1 extends TypeLambda<[n: number], number> { 190 | return: [..._BuildTuple, void>, void]["length"]; 191 | } 192 | type _BuildTuple = 193 | [Length] extends [never] ? never 194 | : Acc["length"] extends Length ? Acc 195 | : _BuildTuple; 196 | 197 | // @ts-expect-error 198 | expect>().to(error); 199 | // @ts-expect-error 200 | expect>().to(error); 201 | } 202 | 203 | { 204 | /* Define utility type-level functions */ 205 | interface NotExtend extends TypeLambda<[x: unknown], boolean> { 206 | return: [Arg0] extends [U] ? false : true; 207 | } 208 | 209 | interface StringLength extends TypeLambda<[s: string], number> { 210 | return: _StringLength>; 211 | } 212 | type _StringLength = 213 | S extends `${string}${infer Tail}` ? _StringLength : Acc["length"]; 214 | 215 | interface CapitalizeString extends TypeLambda<[s: string], string> { 216 | return: Capitalize>; 217 | } 218 | 219 | /* Define type-level functions for filtering, mapping and joining */ 220 | interface Filter> 221 | extends TypeLambda<[xs: Param0[]], Param0[]> { 222 | return: _Filter>; 223 | } 224 | type _Filter = 225 | TS extends [infer Head, ...infer Tail] ? 226 | Call1W extends true ? 227 | _Filter 228 | : _Filter 229 | : Acc; 230 | 231 | interface Map extends TypeLambda<[xs: Param0[]], RetType[]> { 232 | return: _Map>; 233 | } 234 | type _Map = { [K in keyof TS]: Call1W }; 235 | 236 | interface JoinBy extends TypeLambda<[strings: string[]], string> { 237 | return: Arg0 extends [infer S extends string] ? S 238 | : Arg0 extends [infer Head extends string, ...infer Tail extends string[]] ? 239 | `${Head}${Sep}${Call1, Tail>}` 240 | : ""; 241 | } 242 | 243 | /* We can use either `Flow` or `Pipe` to compose type-level functions */ 244 | type ConcatNamesFn = Flow< 245 | Filter>>, 246 | Map, 247 | JoinBy<", "> 248 | >; 249 | expect().to(beOfSig<(xs: string[]) => string>); 250 | 251 | type ConcatNames = Pipe< 252 | Names, 253 | Filter>>, 254 | Map, 255 | JoinBy<", "> 256 | >; 257 | 258 | /* Test the results! */ 259 | expect>().to(equal<"Alice, Bob, Charlie">); 260 | expect>().to(equal<"Alice, Bob, Charlie">); 261 | } 262 | }); 263 | 264 | test("Documentation > Generic type-level functions", () => { 265 | { 266 | /* Types used in previous examples */ 267 | interface NotExtend extends TypeLambda<[x: unknown], boolean> { 268 | return: [Arg0] extends [U] ? false : true; 269 | } 270 | 271 | interface StringLength extends TypeLambda<[s: string], number> { 272 | return: _StringLength>; 273 | } 274 | type _StringLength = 275 | S extends `${string}${infer Tail}` ? _StringLength : Acc["length"]; 276 | 277 | interface CapitalizeString extends TypeLambda<[s: string], string> { 278 | return: Capitalize>; 279 | } 280 | 281 | interface Filter> 282 | extends TypeLambda<[xs: Param0[]], Param0[]> { 283 | return: _Filter>; 284 | } 285 | type _Filter = 286 | TS extends [infer Head, ...infer Tail] ? 287 | Call1W extends true ? 288 | _Filter 289 | : _Filter 290 | : Acc; 291 | 292 | interface Map extends TypeLambda<[xs: Param0[]], RetType[]> { 293 | return: _Map>; 294 | } 295 | type _Map = { [K in keyof TS]: Call1W }; 296 | 297 | interface JoinBy extends TypeLambda<[strings: string[]], string> { 298 | return: Arg0 extends [infer S extends string] ? S 299 | : Arg0 extends [infer Head extends string, ...infer Tail extends string[]] ? 300 | `${Head}${Sep}${Call1, Tail>}` 301 | : ""; 302 | } 303 | /* end */ 304 | 305 | type Names = ["alice", "bob", "i", "charlie", "david"]; 306 | 307 | type _Take = 308 | TS extends [infer Head, ...infer Tail] ? 309 | Counter["length"] extends N ? 310 | [] 311 | : [Head, ..._Take] 312 | : []; 313 | 314 | interface TakeNonGeneric extends TypeLambda<[values: any[]], any[]> { 315 | return: _Take, N>; 316 | } 317 | expect>().to(beOfSig<(values: any[]) => any[]>); 318 | 319 | interface Append extends TypeLambda<[s: string], string> { 320 | return: `${Arg0}${Suffix}`; 321 | } 322 | 323 | type ConcatNamesNonGeneric1 = Flow< 324 | Filter>>, 325 | TakeNonGeneric<3>, 326 | Map, 327 | JoinBy<", ">, 328 | Append<", ..."> 329 | >; 330 | expect>().to(equal<"Alice, Bob, Charlie, ...">); 331 | 332 | interface RepeatString extends TypeLambda<[n: number], string> { 333 | return: _RepeatString>; 334 | } 335 | type _RepeatString = 336 | [Times] extends [never] ? never 337 | : Counter["length"] extends Times ? "" 338 | : `${S}${_RepeatString}`; 339 | 340 | type ConcatNamesNonGeneric2 = Flow< 341 | Filter>>, 342 | TakeNonGeneric<3>, 343 | Map>, 344 | JoinBy<", ">, 345 | Append<", ..."> 346 | >; 347 | expect>().to(equal<`${string}, ...`>); 348 | 349 | interface Take extends TypeLambdaG<["T"]> { 350 | signature: (values: TArg[]) => TArg[]; 351 | return: _Take, N>; 352 | } 353 | expect>().to(beOfSig<(values: T[]) => T[]>); 354 | 355 | type ConcatNamesWrong = Flow< 356 | Filter>>, 357 | Take<3>, 358 | // @ts-expect-error 359 | Map>, 360 | JoinBy<", ">, 361 | Append<", ..."> 362 | >; 363 | 364 | type ConcatNamesRight = Flow< 365 | Filter>>, 366 | Take<3>, 367 | Map, 368 | JoinBy<", ">, 369 | Append<", ..."> 370 | >; 371 | 372 | expect, [string[]]>>().to(equal<{ readonly "~T": string }>); 373 | expect, { 0: number[] }>>().to(equal<{ readonly "~T": number }>); 374 | expect, { r: boolean[] }>>().to(equal<{ readonly "~T": boolean }>); 375 | expect, { 0: string[]; r: number[] }>>().to( 376 | equal<{ readonly "~T": string | number }>, 377 | ); 378 | 379 | expect, { r: string[] }>>().to(equal<{ readonly "~T": string }>); 380 | expect, { 0: string[] }>>().to(equal<{ readonly "~T": string }>); 381 | } 382 | 383 | { 384 | interface Identity extends TypeLambdaG<["T"]> { 385 | signature: (value: TArg) => TArg; 386 | return: Arg0; 387 | } 388 | expect().to(beOfSig<(value: T) => T>); 389 | 390 | interface Map extends TypeLambdaG<["T", "U"]> { 391 | signature: ( 392 | f: TypeLambda<[x: TArg], TArg>, 393 | xs: TArg[], 394 | ) => TArg[]; 395 | return: _Map, Arg1>; 396 | } 397 | type _Map = { [K in keyof TS]: Call1W }; 398 | expect().to(beOfSig<(f: (x: T) => U, xs: T[]) => U[]>); 399 | 400 | interface FromEntries extends TypeLambdaG<[["K", PropertyKey], "V"]> { 401 | signature: ( 402 | entries: [TArg, TArg][], 403 | ) => Record, TArg>; 404 | return: _FromEntries>; 405 | } 406 | type _FromEntries = _PrettifyObject<{ 407 | [K in Entries[number][0]]: Extract[1]; 408 | }>; 409 | type _PrettifyObject = O extends infer U ? { [K in keyof U]: U[K] } : never; 410 | 411 | // Current, `Sig` has some problems and doesn’t fully expand the signature. 412 | // For example, the result of `Sig` is `( 413 | // entries: [TArg, U][], 414 | // ) => Record, U>` 415 | // Though it seems equivalent to the expected signature (i.e., 416 | // `(entries: [K, V][]) => Record`), TypeScript doesn’t 417 | // recognize them as equal. 418 | // Therefore, we do not use `beOfSig` to test the signature of `FromEntries` here, but we test 419 | // several refined signatures in the following tests. 420 | expect>().to( 421 | equal<(entries: [string, number][]) => Record>, 422 | ); 423 | expect }>>().to( 424 | equal<(entries: [number, string | boolean][]) => Record>, 425 | ); 426 | 427 | expect>().to( 428 | equal<{ name: string; age: number }>, 429 | ); 430 | } 431 | }); 432 | 433 | test("Documentation > Type checking in detail > Bypassing strict type checking", () => { 434 | { 435 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 436 | return: `${Arg0}${Arg1}`; 437 | } 438 | 439 | expect>().to(beNever); 440 | } 441 | 442 | { 443 | type Stringifiable = string | number | bigint | boolean | null | undefined; 444 | 445 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 446 | return: RawArg0 extends infer S1 extends Stringifiable ? 447 | RawArg1 extends infer S2 extends Stringifiable ? 448 | `${S1}${S2}` 449 | : never 450 | : never; 451 | } 452 | 453 | expect>().to(equal<"foo42">); 454 | } 455 | 456 | { 457 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 458 | // @ts-expect-error 459 | return: `${RawArg0}${RawArg1}`; 460 | } 461 | } 462 | 463 | { 464 | interface Concat extends TypeLambda<[s1: string, s2: string], number> { 465 | return: `${Arg0}${Arg1}`; 466 | } 467 | 468 | expect>().to(beNever); 469 | expect>().to(equal<"foobar">); 470 | } 471 | }); 472 | 473 | test("Documentation > Type checking in detail > Type checking in `Args`", () => { 474 | { 475 | type JoinString = `${S1}${S2}`; 476 | type JoinStringAndNumber = `${S}${N}`; 477 | 478 | // This is not necessary 479 | interface ConcatRedundant extends TypeLambda<[s1: string, s2: string], string> { 480 | return: Arg0 extends infer S1 extends string ? 481 | Arg1 extends infer S2 extends string ? 482 | JoinString 483 | : never 484 | : never; 485 | } 486 | 487 | // This is enough 488 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 489 | return: JoinString, Arg1>; // OK 490 | } 491 | 492 | // Incompatible type errors are caught by TypeScript 493 | interface ConcatMismatch extends TypeLambda<[s1: string, s2: string], string> { 494 | // @ts-expect-error 495 | return: JoinStringAndNumber, Arg1>; 496 | } 497 | } 498 | 499 | { 500 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 501 | return: [Arg0, Arg1]; // We just print the arguments here for demonstration 502 | } 503 | 504 | expect>().to(equal<["foo", never]>); 505 | } 506 | 507 | { 508 | interface PrintArgs extends TypeLambda<[a: string, b: string], string> { 509 | return: Args; 510 | } 511 | 512 | // Incompatible arguments are cast to `never` 513 | expect>().to(equal<["foo", never]>); 514 | // Redundant arguments are truncated 515 | expect>().to(equal<["foo", "bar"]>); 516 | // Missing arguments are filled with `never` 517 | expect>().to(equal<["foo", never]>); 518 | } 519 | }); 520 | 521 | test("Documentation > Type checking in detail > Type checking in `Apply` and `Call*`", () => { 522 | { 523 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 524 | return: `${Arg0}${Arg1}`; 525 | } 526 | 527 | // Here we return a number, which is incompatible with the declared return type `string` 528 | interface ConcatWrong extends TypeLambda<[s1: string, s2: string], string> { 529 | return: 42; 530 | } 531 | 532 | expect>().to(equal<"foobar">); 533 | expect>().to(beNever); 534 | expect>().to(beNever); 535 | } 536 | 537 | { 538 | interface JoinByRight extends TypeLambda<[strings: string[]], string> { 539 | return: Arg0 extends [infer S] ? S 540 | : Arg0 extends [infer Head extends string, ...infer Tail extends string[]] ? 541 | `${Head}${Sep}${Call1, Tail>}` 542 | : ""; 543 | } 544 | 545 | interface JoinByWrong extends TypeLambda<[strings: string[]], string> { 546 | return: Arg0 extends [infer S] ? S 547 | : Arg0 extends [infer Head extends string, ...infer Tail extends string[]] ? 548 | // @ts-expect-error 549 | `${Head}${Sep}${Call1W, Tail>}` 550 | : ""; 551 | } 552 | } 553 | }); 554 | 555 | test("Documentation > Type checking in detail > Type checking in _generic_ type-level functions", () => { 556 | { 557 | /* Types used in previous examples */ 558 | interface Append extends TypeLambda<[s: string], string> { 559 | return: `${Arg0}${Suffix}`; 560 | } 561 | /* end */ 562 | 563 | interface Map extends TypeLambdaG<["T", "U"]> { 564 | signature: ( 565 | f: TypeLambda<[x: TArg], TArg>, 566 | xs: TArg[], 567 | ) => TArg[]; 568 | return: _Map, Arg1>; 569 | } 570 | type _Map = { [K in keyof TS]: Call1W }; 571 | 572 | type MyApply> = ApplyW; 573 | 574 | // @ts-expect-error 575 | type MapResult = MyApply, ["foo", "bar"]]>; 576 | 577 | expect>().to(equal<[f: TypeLambda<[x: unknown], unknown>, xs: unknown[]]>); 578 | 579 | expect>().to(equal<[f: TypeLambda<[x: never], unknown>, xs: unknown[]]>); 580 | expect>().to(equal); 581 | } 582 | 583 | { 584 | const apply = unknown>(f: F, args: Parameters): ReturnType => 585 | Function.prototype.apply(f, args); 586 | 587 | const map = (f: (x: T) => U, xs: T[]): U[] => xs.map(f); 588 | 589 | // @ts-expect-error 590 | apply(map, [(s: string) => s + "baz", ["foo", "bar"]]); 591 | } 592 | }); 593 | 594 | test("Documentation > Common Utilities > `Always`, `Identity` and `Ask`", () => { 595 | { 596 | type Result = Ok | Err; 597 | type Ok = { _tag: "Ok"; value: T }; 598 | type Err = { _tag: "Err"; error: E }; 599 | 600 | interface MatchResult 601 | extends TypeLambda< 602 | [ 603 | result: Result< 604 | OnOk extends TypeLambda0 ? unknown : Param0, 605 | OnErr extends TypeLambda0 ? unknown : Param0 606 | >, 607 | ], 608 | RetType | RetType 609 | > { 610 | return: Arg0 extends { _tag: "Ok"; value: infer T } ? Call1 611 | : Arg0 extends { _tag: "Err"; error: infer E } ? Call1 612 | : never; 613 | } 614 | 615 | interface Prepend extends TypeLambda<[s: string], string> { 616 | return: `${Prefix}${Arg0}`; 617 | } 618 | 619 | expect, MatchResult, Always<"Oops!">>>>().to(equal<"Mr. Bob">); 620 | expect, MatchResult, Always<"Oops!">>>>().to(equal<"Oops!">); 621 | 622 | expect, MatchResult, Identity>>>().to(equal<"wrong">); 623 | } 624 | 625 | { 626 | expect, Identity>>().to(beOfSig<(value: string) => string>); 627 | expect>>().to(beOfSig<(value: number) => number>); 628 | } 629 | }); 630 | 631 | test("Documentation > Common Utilities > `Tupled` and `Untupled`", () => { 632 | { 633 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 634 | return: `${Arg0}${Arg1}`; 635 | } 636 | expect().to(beOfSig<(s1: string, s2: string) => string>); 637 | expect>().to(equal<"foobar">); 638 | 639 | type TupledConcat = Tupled; 640 | expect().to(beOfSig<(args: [s1: string, s2: string]) => string>); 641 | expect>().to(equal<"foobar">); 642 | } 643 | 644 | { 645 | interface First extends TypeLambdaG<["T"]> { 646 | signature: (pair: [TArg, unknown]) => TArg; 647 | return: Arg0[0]; 648 | } 649 | expect().to(beOfSig<(pair: [T, unknown]) => T>); 650 | expect>().to(equal<42>); 651 | 652 | type UntupledFirst = Untupled; 653 | expect().to(beOfSig<(args_0: T, args_1: unknown) => T>); 654 | expect>().to(equal<42>); 655 | } 656 | }); 657 | 658 | test("Documentation > Common Utilities > `Flip`", () => { 659 | /* Types used in previous examples */ 660 | interface Append extends TypeLambda<[s: string], string> { 661 | return: `${Arg0}${Suffix}`; 662 | } 663 | /* end */ 664 | 665 | interface Map extends TypeLambdaG<["T", "U"]> { 666 | signature: ( 667 | f: TypeLambda<[x: TArg], TArg>, 668 | xs: TArg[], 669 | ) => TArg[]; 670 | return: _Map, Arg1>; 671 | } 672 | type _Map = { [K in keyof TS]: Call1W }; 673 | 674 | { 675 | expect().to(beOfSig<(f: (x: T) => U, xs: T[]) => U[]>); 676 | expect, ["foo", "bar"]>>().to(equal<["foobaz", "barbaz"]>); 677 | 678 | type FlippedMap = Flip; 679 | expect().to(beOfSig<(xs: T[], f: (x: T) => U) => U[]>); 680 | expect>>().to(equal<["foobaz", "barbaz"]>); 681 | } 682 | 683 | { 684 | type CurriedMap = Curry; 685 | expect().to(beOfSig<(f: (x: T) => U) => (xs: T[]) => U[]>); 686 | expect>, ["foo", "bar"]>>().to( 687 | equal<["foobaz", "barbaz"]>, 688 | ); 689 | 690 | type FlippedCurriedMap = Flip; 691 | expect().to(beOfSig<(xs: T[]) => (f: (x: T) => U) => U[]>); 692 | expect, Append<"baz">>>().to( 693 | equal<["foobaz", "barbaz"]>, 694 | ); 695 | } 696 | }); 697 | 698 | test("Documentation > Common Utilities > `Curry`", () => { 699 | /* Types used in previous examples */ 700 | interface Append extends TypeLambda<[s: string], string> { 701 | return: `${Arg0}${Suffix}`; 702 | } 703 | /* end */ 704 | 705 | interface Map extends TypeLambdaG<["T", "U"]> { 706 | signature: ( 707 | f: TypeLambda<[x: TArg], TArg>, 708 | xs: TArg[], 709 | ) => TArg[]; 710 | return: _Map, Arg1>; 711 | } 712 | type _Map = { [K in keyof TS]: Call1W }; 713 | 714 | { 715 | type CurriedMap = Curry; 716 | expect().to(beOfSig<(f: (x: T) => U) => (xs: T[]) => U[]>); 717 | expect>>().to(beOfSig<(xs: string[]) => string[]>); 718 | } 719 | 720 | { 721 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 722 | return: `${Arg0}${Arg1}`; 723 | } 724 | 725 | interface Reduce extends TypeLambdaG<["T", "U"]> { 726 | signature: ( 727 | f: TypeLambda<[acc: TArg, x: TArg], TArg>, 728 | init: TArg, 729 | xs: TArg[], 730 | ) => TArg; 731 | return: _Reduce, Arg1, Arg2>; 732 | } 733 | type _Reduce = 734 | TS extends readonly [infer Head, ...infer Tail] ? _Reduce, Tail> 735 | : Acc; 736 | 737 | expect().to(beOfSig<(f: (acc: U, x: T) => U, init: U, xs: T[]) => U>); 738 | expect>().to(equal<"foobarbaz">); 739 | 740 | type CurriedReduce = Curry; 741 | expect().to( 742 | beOfSig<(f: (acc: U, x: T) => U) => (init: U) => (xs: T[]) => U>, 743 | ); 744 | expect, "">, ["foo", "bar", "baz"]>>().to( 745 | equal<"foobarbaz">, 746 | ); 747 | } 748 | 749 | { 750 | // [f: (x: T) => U](xs: T[]) => U[] 751 | type MapBy = Call1, F>; 752 | expect>>().to(beOfSig<(xs: string[]) => string[]>); 753 | 754 | // [xs: T[]](f: (x: T) => U) => U[] 755 | type MapOn1 = Call1>, TS>; 756 | expect>().to(beOfSig<(f: (x: string) => unknown) => unknown[]>); 757 | 758 | type MapOn2 = Call1>, TS>; 759 | expect>().to(beOfSig<(f: (x: string) => unknown) => unknown[]>); 760 | } 761 | }); 762 | -------------------------------------------------------------------------------- /test/RetType.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import type { Arg0, Arg1, RetType, TypeLambda } from "../src"; 4 | 5 | test("RetType", () => { 6 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 7 | return: `${Arg0}${Arg1}`; 8 | } 9 | 10 | expect>().to(equal); 11 | }); 12 | -------------------------------------------------------------------------------- /test/Sig.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, equal, expect, it } from "typroof"; 2 | 3 | import { beOfSig } from "@hkt-core/typroof-plugin"; 4 | 5 | import type { Arg0, Arg1, Call1W, Sig, TArg, TypeLambda, TypeLambdaG } from "../src"; 6 | 7 | describe("Sig", () => { 8 | it("should infer the signature of a non-generic type-level function", () => { 9 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 10 | return: `${Arg0}${Arg1}`; 11 | } 12 | 13 | expect>().to(equal<(s1: string, s2: string) => string>); 14 | expect().to(beOfSig<(s1: string, s2: string) => string>); 15 | }); 16 | 17 | it("should infer the signature of a generic type-level function with one type parameter", () => { 18 | interface MakeTuple extends TypeLambdaG<["T"]> { 19 | signature: (value: TArg) => [TArg]; 20 | return: [Arg0]; 21 | } 22 | 23 | expect>().to(equal<(value: T) => [T]>); 24 | expect().to(beOfSig<(value: T) => [T]>); 25 | }); 26 | 27 | it("should infer the signature of a generic type-level function with two type parameters", () => { 28 | interface Map extends TypeLambdaG<["T", "U"]> { 29 | signature: ( 30 | f: TypeLambda<[x: TArg], TArg>, 31 | xs: TArg[], 32 | ) => TArg[]; 33 | return: _Map, Arg1>; 34 | } 35 | type _Map = { [K in keyof TS]: Call1W }; 36 | 37 | expect>().to(equal<(f: (x: T) => U, xs: T[]) => U[]>); 38 | expect().to(beOfSig<(f: (x: T) => U, xs: T[]) => U[]>); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/TolerantParams.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import type { Arg0, Arg1, Call1W, TArg, TolerantParams, TypeLambda, TypeLambdaG } from "../src"; 4 | 5 | test("TolerantParams", () => { 6 | interface Map extends TypeLambdaG<["T", "U"]> { 7 | signature: ( 8 | f: TypeLambda<[x: TArg], TArg>, 9 | xs: TArg[], 10 | ) => TArg[]; 11 | return: _Map, Arg1>; 12 | } 13 | type _Map = { [K in keyof TS]: Call1W }; 14 | 15 | expect>().to(equal<[f: TypeLambda<[x: never], unknown>, xs: unknown[]]>); 16 | }); 17 | -------------------------------------------------------------------------------- /test/TolerantRetType.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import { beOfSig } from "@hkt-core/typroof-plugin"; 4 | 5 | import type { 6 | Arg0, 7 | Arg1, 8 | Call1W, 9 | Curry, 10 | TArg, 11 | TolerantRetType, 12 | TypeLambda, 13 | TypeLambdaG, 14 | } from "../src"; 15 | 16 | test("TolerantRetType", () => { 17 | interface Map extends TypeLambdaG<["T", "U"]> { 18 | signature: ( 19 | f: TypeLambda<[x: TArg], TArg>, 20 | xs: TArg[], 21 | ) => TArg[]; 22 | return: _Map, Arg1>; 23 | } 24 | type _Map = { [K in keyof TS]: Call1W }; 25 | 26 | type CurriedMap = Curry; 27 | expect().to(beOfSig<(f: (x: T) => U) => (xs: T[]) => U[]>); 28 | 29 | expect>().to(equal>); 30 | }); 31 | -------------------------------------------------------------------------------- /test/Tupled.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import { beOfSig } from "@hkt-core/typroof-plugin"; 4 | 5 | import type { Apply, Arg0, Arg1, Tupled, TypeLambda } from "../src"; 6 | 7 | test("Tupled", () => { 8 | interface Add extends TypeLambda<[a: number, b: number], number> { 9 | return: [..._BuildTuple>, ..._BuildTuple>]["length"]; 10 | } 11 | type _BuildTuple = 12 | [Length] extends [never] ? never 13 | : Acc["length"] extends Length ? Acc 14 | : _BuildTuple; 15 | 16 | expect().to(beOfSig<(a: number, b: number) => number>); 17 | 18 | type TupledAdd = Tupled; 19 | expect().to(beOfSig<(args: [a: number, b: number]) => number>); 20 | expect>().to(equal<3>); 21 | }); 22 | -------------------------------------------------------------------------------- /test/TypeArgs.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import type { 4 | Arg0, 5 | Arg1, 6 | Call1W, 7 | TArg, 8 | TypeArgs, 9 | TypeLambda, 10 | TypeLambda1, 11 | TypeLambdaG, 12 | } from "../src"; 13 | 14 | test("TypeArgs", () => { 15 | interface Map extends TypeLambdaG<["T", "U"]> { 16 | signature: ( 17 | f: TypeLambda<[x: TArg], TArg>, 18 | xs: TArg[], 19 | ) => TArg[]; 20 | return: _Map, Arg1>; 21 | } 22 | type _Map = { [K in keyof TS]: Call1W }; 23 | 24 | expect>().to( 25 | equal<{ readonly "~T": string } & { readonly ["~U"]: number }>, 26 | ); 27 | expect]>>().to( 28 | equal<{ readonly "~T": number } & { readonly ["~U"]: boolean }>, 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /test/TypeLambda.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, equal, error, expect, it } from "typroof"; 2 | 3 | import { beOfSig } from "@hkt-core/typroof-plugin"; 4 | 5 | import type { Apply, Arg0, Arg1, TypeLambda } from "../src"; 6 | 7 | describe("TypeLambda", () => { 8 | it("should create a TypeLambda that concatenates two strings", () => { 9 | interface Concat extends TypeLambda<[s1: string, s2: string], string> { 10 | return: `${Arg0}${Arg1}`; 11 | } 12 | 13 | expect().to(beOfSig<(s1: string, s2: string) => string>); 14 | 15 | expect>().not.to(error); 16 | expect>().to(equal<"foobar">); 17 | 18 | // @ts-expect-error 19 | expect>().to(error); 20 | }); 21 | 22 | it("should create a TypeLambda that joins an array of strings with a separator", () => { 23 | interface JoinBy extends TypeLambda<[strings: string[]], string> { 24 | return: Arg0 extends [infer S extends string] ? S 25 | : Arg0 extends [infer Head extends string, ...infer Tail extends string[]] ? 26 | `${Head}${Sep}${Apply, [Tail]>}` 27 | : ""; 28 | } 29 | 30 | expect>().not.to(error); 31 | expect>().to(beOfSig<(strings: string[]) => string>); 32 | 33 | expect, [["foo", "bar", "baz"]]>>().not.to(error); 34 | expect, [["foo", "bar", "baz"]]>>().to(equal<"foo, bar, baz">); 35 | }); 36 | 37 | it("should create an untyped version of `Concat`", () => { 38 | interface JustConcat extends TypeLambda { 39 | return: `${Arg0}${Arg1}`; 40 | } 41 | 42 | expect>().not.to(error); 43 | expect>().to(equal<"foobar">); 44 | 45 | expect>().not.to(error); 46 | expect>().to(equal<"foo42">); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/TypeLambdaG.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import { beOfSig } from "@hkt-core/typroof-plugin"; 4 | 5 | import type { Apply, Arg0, Ask, Flow, TArg, TypeLambdaG } from "../src"; 6 | 7 | test("TypeLambdaG", () => { 8 | interface MakeTuple extends TypeLambdaG<["T"]> { 9 | signature: (value: TArg) => [TArg]; 10 | return: [Arg0]; 11 | } 12 | 13 | expect().to(beOfSig<(value: T) => [T]>); 14 | expect>().to(equal<[42]>); 15 | 16 | type WrapStringTuple = Flow, MakeTuple>; 17 | expect().to(beOfSig<(value: string) => [string]>); 18 | expect>().to(equal<["foo"]>); 19 | }); 20 | -------------------------------------------------------------------------------- /test/Untupled.doc.test.ts: -------------------------------------------------------------------------------- 1 | import { equal, expect, test } from "typroof"; 2 | 3 | import { beOfSig } from "@hkt-core/typroof-plugin"; 4 | 5 | import type { Apply, Arg0, TArg, TypeLambdaG, Untupled } from "../src"; 6 | 7 | test("Untupled", () => { 8 | interface First extends TypeLambdaG<["T"]> { 9 | signature: (pair: [TArg, unknown]) => TArg; 10 | return: Arg0[0]; 11 | } 12 | 13 | expect().to(beOfSig<(pair: [T, unknown]) => T>); 14 | expect>().to(equal<42>); 15 | 16 | type UntupledFirst = Untupled; 17 | expect().to(beOfSig<(args_0: T, args_1: unknown) => T>); 18 | expect>().to(equal<42>); 19 | }); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["ESNext"], 6 | "module": "ESNext", 7 | "types": [], 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedIndexedAccess": true, 23 | 24 | /* Type */ 25 | "declaration": true 26 | }, 27 | "include": ["src", "test", "typroof-plugin"] 28 | } 29 | -------------------------------------------------------------------------------- /typroof-plugin/index.ts: -------------------------------------------------------------------------------- 1 | import "./validators"; 2 | 3 | import hktCore from "./plugin"; 4 | 5 | export default hktCore; 6 | 7 | export * from "./matchers"; 8 | -------------------------------------------------------------------------------- /typroof-plugin/matchers.ts: -------------------------------------------------------------------------------- 1 | import { match } from "typroof/plugin"; 2 | 3 | /** 4 | * [Matcher] Check if a TypeLambda’s signature matches a given signature. 5 | * 6 | * Note: Parameter labels are also checked. 7 | * @returns 8 | */ 9 | export const beOfSig = any>(sig?: S) => match<"beOfSig", S>(); 10 | 11 | /** 12 | * [Matcher] Check if a type equals one of the given types. 13 | * @returns 14 | */ 15 | export const beOneOf = < 16 | A = Placeholder, 17 | B = Placeholder, 18 | C = Placeholder, 19 | D = Placeholder, 20 | E = Placeholder, 21 | F = Placeholder, 22 | G = Placeholder, 23 | H = Placeholder, 24 | I = Placeholder, 25 | J = Placeholder, 26 | K = Placeholder, 27 | L = Placeholder, 28 | M = Placeholder, 29 | N = Placeholder, 30 | O = Placeholder, 31 | P = Placeholder, 32 | Q = Placeholder, 33 | R = Placeholder, 34 | S = Placeholder, 35 | T = Placeholder, 36 | U = Placeholder, 37 | V = Placeholder, 38 | W = Placeholder, 39 | X = Placeholder, 40 | Y = Placeholder, 41 | Z = Placeholder, 42 | >( 43 | a?: A, 44 | b?: B, 45 | c?: C, 46 | d?: D, 47 | e?: E, 48 | ) => 49 | match< 50 | "beOneOf", 51 | _ExcludePlaceholders< 52 | [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z] 53 | > 54 | >(); 55 | declare const placeholder: unique symbol; 56 | type Placeholder = typeof placeholder; 57 | type _ExcludePlaceholders = 58 | TS extends readonly [infer T, ...infer Rest] ? 59 | T extends Placeholder ? 60 | _ExcludePlaceholders 61 | : [T, ..._ExcludePlaceholders] 62 | : TS; 63 | 64 | /** 65 | * [Matcher] Check if a type exactly equals another type. 66 | * 67 | * Note: Tuple labels are also checked. 68 | * @returns 69 | */ 70 | export const exactEqual = (y?: U) => match<"exactEqual", U>(); 71 | -------------------------------------------------------------------------------- /typroof-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hkt-core/typroof-plugin", 3 | "private": true, 4 | "description": "Typroof plugin for hkt-core’s internal use for type testing", 5 | "homepage": "https://github.com/Snowflyt/hkt-core/tree/main/typroof-plugin", 6 | "bugs": { 7 | "url": "https://github.com/Snowflyt/hkt-core/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Snowflyt/hkt-core" 12 | }, 13 | "license": "MIT", 14 | "author": "Ge Gao (Snowflyt) ", 15 | "type": "module", 16 | "main": "./index.js", 17 | "types": "./index.d.ts" 18 | } 19 | -------------------------------------------------------------------------------- /typroof-plugin/plugin.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from "typescript"; 2 | import type { Plugin } from "typroof/plugin"; 3 | import { bold } from "typroof/utils/colors"; 4 | 5 | /** 6 | * The hkt-core plugin for Typroof. 7 | * @returns 8 | */ 9 | const hktCore = (): Plugin => ({ 10 | name: "typroof-plugin-hkt-core", 11 | 12 | analyzers: { 13 | beOfSig(actual, expected, { not, typeChecker, validationResult }) { 14 | const actualText = bold(actual.text); 15 | const expectedSignature = bold(typeChecker.typeToString(expected)); 16 | 17 | if (!validationResult) 18 | if (not) 19 | throw `Expect TypeLambda ${actualText} not to be of signature ${expectedSignature}, but does.`; 20 | else 21 | throw `Expect TypeLambda ${actualText} to be of signature ${expectedSignature}, but does not.`; 22 | 23 | const tupleTypes = typeChecker.getTypeArguments(validationResult as ts.TupleType); 24 | const actualSignature = bold(typeChecker.typeToString(tupleTypes[0]!)); 25 | 26 | if (typeChecker.typeToString(tupleTypes[0]!) !== typeChecker.typeToString(tupleTypes[1]!)) 27 | if (not) 28 | throw `Expect TypeLambda ${actualText} not to be of signature ${expectedSignature}, but does.`; 29 | else 30 | throw `Expect TypeLambda ${actualText} to be of signature ${expectedSignature}, but got ${bold( 31 | actualSignature, 32 | )}.`; 33 | }, 34 | 35 | beOneOf(actual, expected, { not, typeChecker }) { 36 | const actualText = bold(actual.text); 37 | const expectedTypes = bold(typeChecker.typeToString(expected).slice(1, -1)); 38 | 39 | throw `Expect type ${actualText} ${not ? "not " : ""}to be one of ${expectedTypes}, but is${ 40 | not ? "" : " not" 41 | }.`; 42 | }, 43 | 44 | exactEqual(actual, expected, { not, typeChecker }) { 45 | if (typeChecker.typeToString(actual.type) === typeChecker.typeToString(expected)) return; 46 | 47 | const actualText = bold(actual.text); 48 | const expectedType = bold(typeChecker.typeToString(expected)); 49 | 50 | let message = `Expect ${actualText}`; 51 | if (actual.text !== typeChecker.typeToString(actual.type)) 52 | message += ` (${bold(typeChecker.typeToString(actual.type))})`; 53 | if (not) message += " not"; 54 | message += ` to exactly equal ${expectedType}, but does`; 55 | if (!not) message += " not"; 56 | message += "."; 57 | 58 | throw message; 59 | }, 60 | }, 61 | }); 62 | 63 | export default hktCore; 64 | -------------------------------------------------------------------------------- /typroof-plugin/validators.ts: -------------------------------------------------------------------------------- 1 | import type { Actual, Expected, IsNegated, Stringify, ToAnalyze, Validator } from "typroof/plugin"; 2 | 3 | import type { Sig } from "../src"; 4 | 5 | /** 6 | * Checks whether `T` exactly equals `U`. 7 | * 8 | * @example 9 | * ```typescript 10 | * type _1 = Equals<1, 1>; 11 | * // ^?: true 12 | * type _2 = Equals<1, number>; 13 | * // ^?: false 14 | * type _3 = Equals<1, 1 | 2>; 15 | * // ^?: false 16 | * ``` 17 | */ 18 | export type Equals = 19 | (() => G extends T ? 1 : 2) extends () => G extends U ? 1 : 2 ? true : false; 20 | 21 | type IsOfSignature = Equals, S> extends true ? ToAnalyze<[Sig, S]> : false; 22 | 23 | type IsOneOf = 24 | TS extends readonly [infer H, ...infer R] ? 25 | Equals extends true ? 26 | true 27 | : IsOneOf 28 | : false; 29 | type StringifyAll = 30 | TS extends readonly [infer Head] ? `\`${Stringify}\`` 31 | : TS extends readonly [infer Head, infer Last] ? 32 | `\`${Stringify}\` or \`${Stringify}\`` 33 | : TS extends readonly [infer Head, ...infer Tail] ? 34 | `\`${Stringify}\`, \`${StringifyAll}\`` 35 | : ""; 36 | 37 | declare module "typroof/plugin" { 38 | interface ValidatorRegistry { 39 | beOfSig: BeOfSigValidator; 40 | beOneOf: BeOneOfValidator; 41 | exactEqual: ExactEqualValidator; 42 | } 43 | } 44 | 45 | interface BeOfSigValidator extends Validator { 46 | return: IsNegated extends false ? 47 | IsOfSignature, Expected> extends false ? 48 | `Expect \`TypeLambda<${Stringify>>}>\` to be of signature \`${Stringify>}\`, but does not` 49 | : IsOfSignature, Expected> 50 | : IsOfSignature, Expected> extends false ? false 51 | : IsOfSignature, Expected>; 52 | } 53 | interface BeOneOfValidator extends Validator { 54 | return: IsNegated extends false ? 55 | IsOneOf, Expected> extends true ? 56 | true 57 | : `Expect \`${Stringify>}\` to be ${StringifyAll>}, but does not` 58 | : IsOneOf, Expected> extends false ? false 59 | : `Expect \`${Stringify>}\` not to be ${StringifyAll>}, but does`; 60 | } 61 | interface ExactEqualValidator extends Validator { 62 | return: IsNegated extends false ? 63 | Equals, Expected> extends true ? 64 | ToAnalyze<[Actual, Expected]> 65 | : `Expect \`${Stringify>}\` to exactly equal \`${Stringify>}\`, but does not` 66 | : Equals, Expected> extends false ? false 67 | : ToAnalyze<[Actual, Expected]>; 68 | } 69 | 70 | export {}; 71 | -------------------------------------------------------------------------------- /typroof.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "typroof/config"; 2 | 3 | import hktCore from "@hkt-core/typroof-plugin"; 4 | 5 | export default defineConfig({ 6 | testFiles: "test/**/*.test.ts", 7 | plugins: [hktCore()], 8 | }); 9 | --------------------------------------------------------------------------------