├── .gitignore ├── docs ├── TODO.md └── changelog.md ├── test ├── mutually-assignable_test.ts ├── predicates_test.ts ├── pedantic-equal_test.ts └── equal_test.ts ├── LICENSE ├── package.json ├── tsconfig.json ├── src └── asserttt.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | # Todos 2 | 3 | * For `assertType(v)`, it would be nice to have a stricter version that insists on `T` being the exact type of `v` (vs. `typeof v` being assignable to `T`). 4 | 5 | ## Potential future additions 6 | 7 | * type-challenges has [several interesting utility types](https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts). 8 | * Looks useful: `Debug` 9 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | * Version 1.0.1 (2025-07-12): 4 | * Simplify implementation of `MutuallyAssignable` 5 | * Version 1.0.0 (2025-07-09): 6 | * Make these generic types non-distributive: `Extends`, `Assignable`, `Includes` 7 | * `Not` and `Not` are `never` 8 | * Better implementation of `Equal` (based on `MutuallyAssignable`) 9 | * New predicate: `MutuallyAssignable` 10 | * New predicate: `PedanticEqual` 11 | * New predicate: `IsAny` 12 | -------------------------------------------------------------------------------- /test/mutually-assignable_test.ts: -------------------------------------------------------------------------------- 1 | // * All tests in this file run at compile/editing time. 2 | // * Therefore, the tests succeed if you open it in a TypeScript editor and 3 | // see no errors. 4 | 5 | import { type Assert, type MutuallyAssignable, type Not } from 'asserttt'; 6 | 7 | { 8 | type Pair = [X, X]; 9 | type _ = [ 10 | Assert, ['a', 'a'] 12 | >>, 13 | Assert, ['x', 'x'] 15 | >>>, 16 | ]; 17 | } 18 | { 19 | type _ = [ 20 | Assert>>, 23 | Assert>, 26 | Assert>, 29 | Assert>, 32 | Assert>, 35 | Assert>, 38 | ]; 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Axel Rauschmayer 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asserttt", 3 | "version": "1.0.1", 4 | "type": "module", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/rauschma/asserttt.git" 9 | }, 10 | "author": "Axel Rauschmayer", 11 | "exports": { 12 | ".": "./dist/src/asserttt.js" 13 | }, 14 | "// files": [ 15 | "We can jump to TS source code, thanks to declarationMap:true in tsconfig.json", 16 | "src/asserttt.ts", 17 | "dist/src/asserttt.js", 18 | "dist/src/asserttt.js.map", 19 | "dist/src/asserttt.d.ts", 20 | "dist/src/asserttt.d.ts.map" 21 | ], 22 | "files": [ 23 | "package.json", 24 | "README.md", 25 | "LICENSE", 26 | "src/**/*.ts", 27 | "dist/**/*.js", 28 | "dist/**/*.js.map", 29 | "dist/**/*.d.ts", 30 | "dist/**/*.d.ts.map", 31 | "!src/**/*_test.ts", 32 | "!dist/**/*_test.js", 33 | "!dist/**/*_test.js.map", 34 | "!dist/**/*_test.d.ts", 35 | "!dist/**/*_test.d.ts.map" 36 | ], 37 | "scripts": { 38 | "\n========== Building ==========": "", 39 | "build": "npm run clean && tsc", 40 | "watch": "tsc --watch", 41 | "clean": "shx rm -rf ./dist/*", 42 | "\n========== Publishing ==========": "", 43 | "prepublishOnly": "npm run build", 44 | "publishd": "npm publish --dry-run", 45 | "packd": "npm pack --dry-run" 46 | }, 47 | "devDependencies": { 48 | "shx": "^0.3.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/predicates_test.ts: -------------------------------------------------------------------------------- 1 | // * All tests in this file run at compile/editing time. 2 | // * Therefore, the tests succeed if you open it in a TypeScript editor and 3 | // see no errors. 4 | 5 | import { type Assert, assertType, type Assignable, type Equal, type Extends, type Includes, type IsAny, type Not } from 'asserttt'; 6 | 7 | //========== Asserting types of values: assertType(v) ========== 8 | 9 | const n = 3 + 1; 10 | assertType(n); 11 | 12 | //========== Assert and type comparison predicates ========== 13 | 14 | { 15 | type _ = [ 16 | Assert>, 17 | 18 | Assert, Object>>, 19 | Assert, RegExp>>>, 20 | Assert>, 21 | // Must not trigger distributivity (left-hand side of `extends`) 22 | Assert>>, 23 | 24 | Assert>, 25 | // Must not trigger distributivity (left-hand side of `extends`) 26 | Assert>>, 27 | ]; 28 | } 29 | 30 | //========== Predicate `Not` ========== 31 | 32 | { 33 | type _ = [ 34 | Assert, false>>, 35 | Assert, true>>, 36 | Assert, never>>, 37 | Assert, never>>, 38 | ]; 39 | } 40 | 41 | //========== Predicate `IsAny` ========== 42 | 43 | { 44 | type _ = [ 45 | Assert>, 46 | 47 | Assert>>, 48 | Assert>>, 49 | Assert>>, 50 | Assert>>, 51 | ]; 52 | } 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*", "test/**/*"], 3 | "compilerOptions": { 4 | // Specified explicitly (not derived from source file paths) 5 | "rootDir": ".", 6 | "outDir": "dist", 7 | 8 | //========== Target and module ========== 9 | // Nothing is ever transpiled 10 | "target": "ESNext", // sets up "lib" accordingly 11 | "module": "NodeNext", // sets up "moduleResolution" 12 | // Don’t check .d.ts files 13 | "skipLibCheck": true, 14 | // Emptily imported modules must exist 15 | "noUncheckedSideEffectImports": true, 16 | // Allow importing JSON 17 | "resolveJsonModule": true, 18 | 19 | //========== Type checking ========== 20 | // Essential: activates several useful options 21 | "strict": true, 22 | // Beyond "strict": less important 23 | "exactOptionalPropertyTypes": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noImplicitOverride": true, 26 | "noImplicitReturns": true, 27 | "noPropertyAccessFromIndexSignature": true, 28 | "noUncheckedIndexedAccess": true, 29 | 30 | //========== Emitted files ========== 31 | //----- Output: .js ----- 32 | "sourceMap": true, // .js.map files 33 | //----- Output: .d.ts ----- 34 | "declaration": true, // .d.ts files 35 | // “Go to definition” jumps to TS source etc. 36 | "declarationMap": true, // .d.ts.map files 37 | // - Enforces constraints that enable efficient .d.ts generation: 38 | // no inferred return types for exported functions etc. 39 | // - Even though this option would be generally useful, it requires 40 | // that `declaration` and/or `composite` are true. 41 | "isolatedDeclarations": true, 42 | } 43 | } -------------------------------------------------------------------------------- /test/pedantic-equal_test.ts: -------------------------------------------------------------------------------- 1 | // * All tests in this file run at compile/editing time. 2 | // * Therefore, the tests succeed if you open it in a TypeScript editor and 3 | // see no errors. 4 | 5 | import { type Assert, type Assignable, type Equal, type Extends, type Not, type PedanticEqual } from 'asserttt'; 6 | 7 | { // `PedanticEqual` is not useful with intersections 8 | type Point = { x: number } & { y: number }; 9 | type _ = [ 10 | Assert>>, 13 | Assert>, 16 | ]; 17 | } 18 | 19 | { // `PedanticEqual` bug with `exactOptionalPropertyTypes` 20 | type T1 = { 21 | prop?: string, 22 | } 23 | type T2 = { 24 | prop?: undefined | string, 25 | } 26 | type _ = [ 27 | // ❌ Bug: T1 and T2 are considered pedantically equal 28 | Assert>, 31 | // T1 and T2 are not considered loosely equal 32 | Assert>>, 35 | // T2 is not assignable to T1 36 | Assert>>, 39 | // T1 is assignable to T2 40 | Assert>, 43 | ]; 44 | } 45 | 46 | { // `PedanticEqual` is not useful with enums 47 | enum NoYes { No, Yes } 48 | type _ = [ 49 | Assert>>, 50 | 51 | Assert>, 52 | Assert>, 53 | Assert>, 54 | ]; 55 | } 56 | 57 | { // Rare quirk 58 | type _ = [ 59 | Assert>>, 63 | ]; 64 | } -------------------------------------------------------------------------------- /test/equal_test.ts: -------------------------------------------------------------------------------- 1 | // * All tests in this file run at compile/editing time. 2 | // * Therefore, the tests succeed if you open it in a TypeScript editor and 3 | // see no errors. 4 | 5 | import { type Assert, type Equal, type MutuallyAssignable, type Not } from 'asserttt'; 6 | 7 | { 8 | type Pair = [X, X]; 9 | type _ = [ 10 | Assert, ['a', 'a']>>, 11 | Assert, ['x', 'x']>>>, 12 | ]; 13 | } 14 | 15 | { 16 | // - `MutuallyAssignable` considers `any` to be equal to other types. 17 | // - `Equal` doesn’t. 18 | type _ = [ 19 | Assert>>, 20 | Assert>>, 21 | Assert>, 22 | Assert>, 23 | ]; 24 | } 25 | 26 | { // There must be no distributivity. 27 | // Try to trigger it for both type parameters. 28 | type _ = [ 29 | Assert>>, 30 | Assert>>, 31 | ]; 32 | } 33 | 34 | //========== `PedanticEqual` bugs that `Equal` doesn’t have ========== 35 | 36 | { // Works with with intersections 37 | type Point = { x: number } & { y: number }; 38 | type _ = [ 39 | Assert>, 40 | ]; 41 | } 42 | 43 | { // Honor `exactOptionalPropertyTypes` in tsconfig.json 44 | type T1 = { 45 | prop?: string, 46 | } 47 | type T2 = { 48 | prop?: undefined | string, 49 | } 50 | type _ = [ 51 | // T1 and T2 must not be considered equal 52 | Assert>>, 55 | ]; 56 | } 57 | 58 | { // Works with enums 59 | enum NoYes { No, Yes } 60 | type _ = [ 61 | Assert>, 62 | ]; 63 | } 64 | 65 | { // Rare quirk 66 | type _ = [ 67 | Assert>, 71 | ]; 72 | } 73 | -------------------------------------------------------------------------------- /src/asserttt.ts: -------------------------------------------------------------------------------- 1 | //========== Asserting ========== 2 | 3 | /** 4 | * - Is type `_T` assignable to `true` 5 | * - Based on code by Blaine Bublitz. 6 | */ 7 | export type Assert<_T extends true> = void; 8 | 9 | /** 10 | * Is the type of `_value` assignable to `T`? 11 | */ 12 | export function assertType(_value: T): void { } 13 | 14 | //========== Predicates: equality ========== 15 | 16 | 17 | /** 18 | * - Is type `X` equal to type `Y` (with `any` only being equal to itself)? 19 | * - Name motivated by Node’s assert.equal(). 20 | * - Like `MutuallyAssignable` but `any` is only equal to itself. 21 | */ 22 | export type Equal = 23 | [IsAny, IsAny] extends [true, true] ? true 24 | : [IsAny, IsAny] extends [false, false] ? MutuallyAssignable 25 | : false 26 | ; 27 | 28 | /** 29 | * - The brackets on the left-hand side of `extends` prevent 30 | * distributivity. 31 | */ 32 | export type MutuallyAssignable = 33 | [X, Y] extends [Y, X] ? true : false 34 | ; 35 | 36 | /** 37 | * - Name motivated by Node’s assert.deepEqual(). 38 | * - Source: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650 39 | */ 40 | export type PedanticEqual = 41 | (() => T extends X ? 1 : 2) extends 42 | (() => T extends Y ? 1 : 2) ? true : false 43 | ; 44 | 45 | //========== Predicates: comparing types ========== 46 | 47 | /** 48 | * - Does type `Sub` extend type `Super`? 49 | * - Square brackets around `Sub` prevent distributivity (`Super` is also 50 | * in brackets so that the test works). 51 | */ 52 | export type Extends = [Sub] extends [Super] ? true : false; 53 | 54 | /** 55 | * - Is type `Target` assignable from type `Source`? 56 | * - Square brackets around `Source` prevent distributivity (`Target` is 57 | * also in brackets so that the test works). 58 | */ 59 | export type Assignable = [Source] extends [Target] ? true : false; 60 | 61 | /** 62 | * - Is type `Subset` a subset of type `Superset`? 63 | * - Square brackets around `Subset` prevent distributivity (`Superset` is 64 | * also in brackets so that the test works). 65 | */ 66 | export type Includes = [Subset] extends [Superset] ? true : false; 67 | 68 | //========== Predicates: boolean operations ========== 69 | 70 | /** 71 | * Boolean NOT for type `B`. 72 | */ 73 | export type Not = 74 | // `Equal` because want to avoid Not being `false` or `true` 75 | Equal extends true 76 | ? false 77 | : (Equal extends true ? true : never) 78 | ; 79 | 80 | //========== Predicates: other ========== 81 | 82 | /** 83 | * - Source: https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 84 | */ 85 | export type IsAny = 0 extends (1 & T) ? true : false; 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asserttt: minimal API for testing types 2 | 3 | --- 4 | 5 | Table of contents: 6 | 7 | * [Installation](#installation) 8 | * [Use cases](#use-cases) 9 | * [Complementary tools](#complementary-tools) 10 | * [Usage](#usage) 11 | * [API](#api) 12 | * [How does the code work?](#how-does-the-code-work) 13 | * [Related work](#related-work) 14 | 15 | --- 16 | 17 | 18 | 19 | ## Installation 20 | 21 | ```js 22 | npm install asserttt 23 | ``` 24 | 25 | `asserttt` has no non-dev dependencies! 26 | 27 | 28 | 29 | ## Use cases 30 | 31 | * Testing types 32 | * Documenting TypeScript with code examples (e.g. via [Markcheck](https://github.com/rauschma/markcheck)) 33 | * Support for TypeScript exercises, such as the ones provided by [`Type`](https://tsch.js.org). 34 | * [Testing that related types are consistent](https://exploringjs.com/ts/book/ch_testing-types.html#type-level-assertions-in-normal-code) in normal code 35 | 36 | 37 | 38 | ## Complementary tools 39 | 40 | If a file contains type tests, it’s not enough to run it, we must also type-check it: 41 | 42 | * [tsx (TypeScript Execute)](https://www.npmjs.com/package/tsx) is a tool that type-checks files before running them. 43 | * It works well with the Mocha test runner: [example setup](https://github.com/mochajs/mocha-examples/tree/main/packages/typescript-tsx-esm-import) 44 | * [ts-expect-error](https://www.npmjs.com/package/ts-expect-error) performs two tasks: 45 | * Checking if each `@ts-expect-error` annotation prevents the right kind of error 46 | * Optional: reporting errors detected by TypeScript 47 | * [Markcheck](https://github.com/rauschma/markcheck) tests Markdown code blocks. 48 | 49 | 50 | 51 | ## Usage 52 | 53 | ```ts 54 | import { type Assert, assertType, type Assignable, type Equal, type Extends, type Includes, type Not } from 'asserttt'; 55 | 56 | //========== Asserting types: Assert ========== 57 | 58 | { 59 | type Pair = [X, X]; 60 | type _1 = Assert, ['a', 'a'] 62 | >>; 63 | type _2 = Assert, ['x', 'x'] 65 | >>>; 66 | } 67 | 68 | { 69 | type _ = [ 70 | Assert>, 71 | 72 | Assert, Object>>, 73 | Assert, RegExp>>>, 74 | 75 | Assert>, 76 | Assert>, 77 | ]; 78 | } 79 | 80 | //========== Asserting types of values: assertType(v) ========== 81 | 82 | const n = 3 + 1; 83 | assertType(n); 84 | ``` 85 | 86 | 87 | 88 | ## API 89 | 90 | ### Asserting 91 | 92 | * `Assert` 93 | * `assertType(value)` 94 | 95 | ### Included _predicates_ (boolean results) 96 | 97 | Equality: 98 | 99 | * `Equal` 100 | * `MutuallyAssignable` 101 | * `PedanticEqual` 102 | 103 | Comparing/detecting types: 104 | 105 | * `Extends` 106 | * `Assignable` 107 | * `Includes` 108 | * `IsAny` 109 | 110 | Boolean operations: 111 | 112 | * `Not` 113 | 114 | 115 | 116 | ## How does the code work? 117 | 118 | 119 | 120 | ### Determining if two types are equal 121 | 122 | #### `MutuallyAssignable` 123 | 124 | ```ts 125 | export type MutuallyAssignable = 126 | [X, Y] extends [Y, X] ? true : false 127 | ; 128 | ``` 129 | 130 | * The brackets on the left-hand side of `extends` prevent distributivity. 131 | * Almost what we want for checking equality, but `any` is equal to all types – which is problematic when testing types. 132 | 133 | #### `Equal`: like `MutuallyAssignable` but `any` is only equal to itself 134 | 135 | This `Equal` predicate works well for many use cases: 136 | 137 | ```ts 138 | type Equal = 139 | [IsAny, IsAny] extends [true, true] ? true 140 | : [IsAny, IsAny] extends [false, false] ? MutuallyAssignable 141 | : false 142 | ; 143 | type IsAny = 0 extends (1 & T) ? true : false; 144 | ``` 145 | 146 | * [Explanation of `Equal`](https://exploringjs.com/ts/book/ch_testing-types.html#checking-type-equality) 147 | * [Explanation of `IsAny`](https://exploringjs.com/ts/book/ch_testing-types.html#checking-if-type-is-any). 148 | 149 | #### `PedanticEqual`: a popular hack with several downsides 150 | 151 | ```ts 152 | type PedanticEqual = 153 | (() => T extends X ? 1 : 2) extends // (A) 154 | (() => T extends Y ? 1 : 2) ? true : false // (B) 155 | ; 156 | ``` 157 | 158 | It was suggested by Matt McCutchen ([source](https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650)). How does it work ([source](https://github.com/microsoft/TypeScript/issues/27024#issuecomment-510924206))? 159 | 160 | In order to check whether the function type in line A extends the function type in line B, TypeScript has to compare the following two conditional types: 161 | 162 | ```ts 163 | T extends X ? 1 : 2 164 | T extends Y ? 1 : 2 165 | ``` 166 | 167 | Since `T` does not have a value, both conditional types are _deferred_. Assignability of two deferred conditional types is computed via the internal function `isTypeIdenticalTo()` and only `true` if: 168 | 169 | 1. Both have the same constraint. 170 | 2. Their “then” branches have the same type and their “else” branches have the same type. 171 | 172 | Thanks to #1, `X` and `Y` are compared precisely. 173 | 174 | **This hack has several downsides:** See [`test/pedantic-equal_test.ts`](test/pedantic-equal_test.ts) for more information. 175 | 176 | 177 | 178 | ### Asserting 179 | 180 | ```ts 181 | type Assert<_T extends true> = void; 182 | ``` 183 | 184 | Alas, we can’t conditionally produce errors at the type level. That’s why we need to resort to a type parameter whose `extends` constraint requires it to be assignable to `true`. 185 | 186 | (Idea by Blaine Bublitz) 187 | 188 | 189 | 190 | ## Related work 191 | 192 | * Package [ts-expect](https://github.com/TypeStrong/ts-expect) inspired this package. It’s very similar. This package uses different names and has a utility type `Assert` (which doesn’t produce runtime code): 193 | ```ts 194 | type _ = Assert>; // asserttt 195 | expectType>(true); // ts-expect 196 | ``` 197 | 198 | * The type-challenges repository has [a module with utility types for exercises](https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts). How is asserttt different? 199 | * Smaller API 200 | * Different names 201 | * Implements boolean NOT via a helper type `Not` (vs. two versions of the same utility type). 202 | 203 | * [eslint-plugin-expect-type](https://www.npmjs.com/package/eslint-plugin-expect-type) supports an elegant notation but requires a special tool (eslint) for checking. 204 | --------------------------------------------------------------------------------