{"id":19675309,"url":"https://github.com/snowflyt/typroof","last_synced_at":"2025-07-28T22:39:59.189Z","repository":{"id":207541209,"uuid":"719500326","full_name":"Snowflyt/typroof","owner":"Snowflyt","description":"TypeScript type testing with a fast CLI tool and a smooth WYSIWYG editor experience.","archived":false,"fork":false,"pushed_at":"2025-05-08T06:47:48.000Z","size":32875,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-14T07:53:48.134Z","etag":null,"topics":["check","static-analysis","test","typescript","typroof"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/typroof","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Snowflyt.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-16T09:54:16.000Z","updated_at":"2025-05-31T11:38:59.000Z","dependencies_parsed_at":null,"dependency_job_id":"c771f273-78ec-4dbc-961f-46d994dd5f80","html_url":"https://github.com/Snowflyt/typroof","commit_stats":null,"previous_names":["snowfly-t/typroof","snowflyt/typroof"],"tags_count":28,"template":false,"template_full_name":null,"purl":"pkg:github/Snowflyt/typroof","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftyproof","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftyproof/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftyproof/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftyproof/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Snowflyt","download_url":"https://codeload.github.com/Snowflyt/typroof/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftyproof/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267598726,"owners_count":24113653,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-28T02:00:09.689Z","response_time":68,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["check","static-analysis","test","typescript","typroof"],"created_at":"2024-11-11T17:23:06.346Z","updated_at":"2025-07-28T22:39:59.182Z","avatar_url":"https://github.com/Snowflyt.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003eTyproof\u003c/h1\u003e\n\nTypeScript **type testing** with a fast **CLI** tool and a smooth **WYSIWYG editor experience**.\n\n[![downloads](https://img.shields.io/npm/dm/typroof.svg?style=flat\u0026colorA=000000\u0026colorB=000000)](https://www.npmjs.com/package/typroof)\n[![version](https://img.shields.io/npm/v/typroof.svg?style=flat\u0026colorA=000000\u0026colorB=000000)](https://www.npmjs.com/package/typroof)\n[![test](https://img.shields.io/github/actions/workflow/status/Snowflyt/typroof/ci.yml?label=test\u0026style=flat\u0026colorA=000000\u0026colorB=000000)](https://github.com/Snowflyt/typroof/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/typroof.svg?style=flat\u0026colorA=000000\u0026colorB=000000)](https://github.com/Snowflyt/typroof)\n\nhttps://github.com/user-attachments/assets/53aa8f97-c580-428e-89b2-2d07d1c5680d\n\n## Installation\n\n```shell\nnpm install --save-dev typroof\n```\n\n## Quickstart\n\n### Define your types\n\nAssume that you write a `string-utils.ts` file with the following type definitions:\n\n```typescript\nexport type Append\u003cS extends string, Ext extends string\u003e = `${S}${Ext}`;\nexport type Prepend\u003cS extends string, Start extends string\u003e = `${Start}${S}`;\n\nexport const append = \u003cS extends string, Ext extends string\u003e(s: S, ext: Ext): Append\u003cS, Ext\u003e =\u003e\n  `${s}${ext}`;\n```\n\n### Add a type test\n\nCreate a `string-utils.proof.ts` file in the same directory to test them:\n\n```typescript\nimport { describe, equal, error, expect, extend, it, test } from 'typroof';\nimport { append } from './string-utils';\nimport type { Append, Prepend } from './string-utils';\n\ntest('Append', () =\u003e {\n  expect\u003cAppend\u003c'foo', 'bar'\u003e\u003e().to(equal\u003c'foo'\u003e);\n  expect\u003cAppend\u003c'foo', 'bar'\u003e\u003e().to(extend\u003cstring\u003e);\n  expect\u003cAppend\u003c'foo', 'bar'\u003e\u003e().not.to(extend\u003cnumber\u003e);\n  expect(append('foo', 'bar')).to(equal('foobar' as const));\n});\n```\n\nOops! Seems we have made a mistake, and TypeScript language server is already showing you the error message in your editor:\n\n```typescript\nexpect\u003cAppend\u003c'foo', 'bar'\u003e\u003e().to(equal\u003c'foo'\u003e);\n//                                ~~~~~~~~~~~~\n//            Argument of type '...' is not assignable to parameter\n//         of type '\"Expect `'foobar'` to equal `'foo'`, but does not\"'\n```\n\nThis is the **WYSIWYG editor experience** Typroof provides—instant feedback right in your editor.\n\nLet’s ignore this error for now and see what happens when we run the tests.\n\n### Run the CLI\n\nRun `typroof` to test your type definitions:\n\n```shell\nnpx typroof\n```\n\nYou’ll see the error clearly reported:\n\n```shell\n❯ src/string-utils.proof.ts (1)\n  × Append\n    ❯ src/string-utils.proof.ts:6:37\n      Expect Append\u003c'foo', 'bar'\u003e to equal \"foo\", but got \"foobar\".\n\n Test Files  1 failed (1)\n      Tests  1 failed (1)\n   Start at  17:50:11\n   Duration  2ms\n```\n\nLet’s fix the error in the test file:\n\n```diff\n- expect\u003cAppend\u003c'foo', 'bar'\u003e\u003e().to(equal\u003c'foo'\u003e);\n+ expect\u003cAppend\u003c'foo', 'bar'\u003e\u003e().to(equal\u003c'foobar'\u003e);\n```\n\nSuccess! You’ve written and verified your first type test with Typroof. 🎉\n\n```shell\n✓ src/string-utils.proof.ts (1)\n  ✓ Append\n\n Test Files  1 passed (1)\n      Tests  1 passed (1)\n   Start at  17:51:26\n   Duration  2ms\n```\n\n\u003e [!NOTE]\n\u003e\n\u003e By default, Typroof does not perform type checking on test files. This allows matchers like `.to(error)` to work properly without requiring manual `@ts-expect-error` comments. If you want to enable type checking on your test files, you can use the `--check` option when running Typroof:\n\u003e\n\u003e ```shell\n\u003e npx typroof --check\n\u003e ```\n\n## Usage\n\nAfter getting started with Typroof, let’s explore its core concepts and patterns in more depth.\n\n### API Overview\n\nTyproof provides a familiar testing API that resembles Jest:\n\n- **`test`/`it`**: Create individual test cases\n- **`describe`**: Group related tests together\n- **`expect`**: Create an assertion on a type or value\n- **`.to(matcher)`**: Apply a matcher to validate the assertion\n- **`.not.to(matcher)`**: Negate a matcher expectation\n\n### Assertion Patterns\n\nTyproof offers flexible ways to write assertions:\n\n```typescript\n// Testing a type directly (most common for type utilities)\nexpect\u003cMyType\u003cInput\u003e\u003e().to(equal\u003cExpected\u003e);\n\n// Testing a value’s type (useful for functions)\nexpect(myFunction('input')).to(equal\u003cExpectedReturnType\u003e);\n\n// Negating an assertion\nexpect\u003cMyType\u003e().not.to(beAny);\n\n// Testing for errors\n// @ts-expect-error\nexpect\u003cInvalidType\u003e().to(error);\n```\n\n### Matcher Flexibility\n\nMatchers can receive expected types in two ways:\n\n```typescript\n// As a type parameter (preferred for testing generic types)\nexpect\u003cMyType\u003c'input'\u003e\u003e().to(equal\u003c'expected'\u003e);\n\n// As a value parameter (useful for function return types)\nconst result = computeSomething();\nconst expected = computeSomethingElse();\nexpect(result).to(equal(expected));\n```\n\n### Composing Tests\n\n```typescript\ndescribe('StringUtils', () =\u003e {\n  describe('Append', () =\u003e {\n    it('concatenates two strings', () =\u003e {\n      expect\u003cAppend\u003c'hello', ' world'\u003e\u003e().to(equal\u003c'hello world'\u003e);\n    });\n\n    it('returns a string type', () =\u003e {\n      expect\u003cAppend\u003c'a', 'b'\u003e\u003e().to(extend\u003cstring\u003e);\n    });\n  });\n\n  describe('Split', () =\u003e {\n    it('splits a string into tuple', () =\u003e {\n      expect\u003cSplit\u003c'a-b-c', '-'\u003e\u003e().to(equal\u003c['a', 'b', 'c']\u003e);\n    });\n  });\n});\n```\n\n### Testing For Type Errors\n\nTest that invalid types correctly produce errors:\n\n```typescript\ndescribe('NumericUtilities', () =\u003e {\n  it('rejects non-numeric inputs', () =\u003e {\n    // @ts-expect-error - string is not assignable to number\n    expect\u003cAdd\u003c'not-a-number', 5\u003e\u003e().to(error);\n  });\n});\n```\n\n### Running Tests\n\nRun tests with the Typroof CLI:\n\n```shell\nnpx typroof [optional path]\n```\n\nBy default, Typroof will find all `.proof.ts` files or files in `proof/` directories and analyze them. To customize which files to test, you can create a `typroof.config.ts` file in the root directory of your project. See [Configuration](#configuration) for more information.\n\n## Matchers\n\nMatchers are the core of Typroof’s assertion system. They let you validate type relationships and characteristics in your tests.\n\n### Matcher Basics\n\nEach matcher can be used with the `expect().to()` or `expect().not.to()` syntax:\n\n```typescript\n// Basic matcher usage\nexpect\u003cMyType\u003e().to(matcherName\u003cOptionalTypeArg\u003e);\n\n// Negated matcher usage\nexpect\u003cMyType\u003e().not.to(matcherName\u003cOptionalTypeArg\u003e);\n```\n\n### Built-in Matchers\n\nTyproof provides two categories of matchers for different testing needs:\n\n#### Equality and Basic Type Matchers\n\n| Matcher            | Description                                            | Example                                                    |\n| ------------------ | ------------------------------------------------------ | ---------------------------------------------------------- |\n| `equal\u003cT\u003e`         | Checks for exact type equality                         | `expect\u003c'hello'\u003e().to(equal\u003c'hello'\u003e)`                     |\n| `error`            | Verifies a type produces a compilation error           | `expect\u003cConcatString\u003c'foo', 42\u003e\u003e().to(error)`              |\n| `beAny`            | Checks if a type is `any`                              | `expect\u003cany\u003e().to(beAny)`                                  |\n| `beNever`          | Checks if a type is `never`                            | `expect\u003cnever\u003e().to(beNever)`                              |\n| `beNull`           | Checks if a type is `null`                             | `expect\u003cnull\u003e().to(beNull)`                                |\n| `beUndefined`      | Checks if a type is `undefined`                        | `expect\u003cundefined\u003e().to(beUndefined)`                      |\n| `beNullish`        | Checks if a type is `null`, `undefined` or their union | \u003ccode\u003eexpect\u003cnull \u0026#124; undefined\u003e().to(beNullish)\u003c/code\u003e |\n| `beTrue`/`beFalse` | Checks if a type is `true`/`false`                     | `expect\u003ctrue\u003e().to(beTrue)`                                |\n| `matchBoolean`     | Checks if a type is `true`, `false` or `boolean`       | `expect\u003cboolean\u003e().to(matchBoolean)`                       |\n\n#### Type Relationship Matchers\n\n| Matcher           | Description                                   | Example                                     |\n| ----------------- | --------------------------------------------- | ------------------------------------------- |\n| `extend\u003cT\u003e`       | Checks if a type is assignable to another     | `expect\u003c'hello'\u003e().to(extend\u003cstring\u003e)`      |\n| `strictExtend\u003cT\u003e` | Like `extend` but stricter with `any`/`never` | `expect\u003cstring\u003e().to(strictExtend\u003cstring\u003e)` |\n| `cover\u003cT\u003e`        | Checks if a type is a supertype of another    | `expect\u003cstring\u003e().to(cover\u003c'hello'\u003e)`       |\n| `strictCover\u003cT\u003e`  | Like `cover` but stricter with `any`/`never`  | `expect\u003cstring\u003e().to(strictCover\u003c'hello'\u003e)` |\n\n### Matcher Examples\n\nTesting type utilities:\n\n```typescript\n// Testing a string template utility\ntype Prefix\u003cT extends string, P extends string\u003e = `${P}${T}`;\n\ntest('Prefix type', () =\u003e {\n  expect\u003cPrefix\u003c'World', 'Hello '\u003e\u003e().to(equal\u003c'Hello World'\u003e);\n  expect\u003cPrefix\u003c'file', 'index.'\u003e\u003e().to(extend\u003cstring\u003e);\n  expect\u003cPrefix\u003c'foo', 'bar'\u003e\u003e().not.to(equal\u003c'foobar'\u003e);\n});\n```\n\nTesting for compilation errors:\n\n```typescript\n// Testing constraint violations\ntest('NumericId constraints', () =\u003e {\n  type NumericId\u003cT extends number\u003e = T;\n\n  expect\u003cNumericId\u003c42\u003e\u003e().to(extend\u003cnumber\u003e);\n\n  // @ts-expect-error - String not assignable to number\n  expect\u003cNumericId\u003c'42'\u003e\u003e().to(error);\n});\n```\n\n### Understanding Special Types\n\nTypeScript’s `any` and `never` types have special behavior in type relationships:\n\n- `any` is both a subtype and supertype of all types.\n- `never` is a subtype of all types but has no subtypes.\n\nThis can lead to unexpected results in type tests:\n\n```typescript\n// Regular extend allows any to be assigned to anything\nexpect\u003cany\u003e().to(extend\u003cstring\u003e); // passes\n\n// strictExtend prevents this\nexpect\u003cany\u003e().not.to(strictExtend\u003cstring\u003e); // passes\n```\n\nFor more predictable behavior with these types, use `strictExtend` and `strictCover` when testing type relationships involving `any` or `never`.\n\n## Configuration\n\nTyproof can be customized through a configuration file to control which files are tested and how tests are run.\n\n### Configuration File\n\nCreate a `typroof.config.ts` file in your project root:\n\n```typescript\nimport { defineConfig } from 'typroof/config';\n\nexport default defineConfig({\n  testFiles: '**/*.types.test.ts',\n});\n```\n\nYou can use either `.ts`, `.mts`, `.cts`, `.js`, `.mjs` or `.cjs` as the extension of the config file. The priority is `.ts` \u003e `.mts` \u003e `.cts` \u003e `.js` \u003e `.mjs` \u003e `.cjs`.\n\n### Available Options\n\n| Option             | Type                                | Default                                          | Description                                                                         |\n| ------------------ | ----------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------- |\n| `tsConfigFilePath` | `string`                            | `'./tsconfig.json'`                              | Path to the TypeScript configuration file                                           |\n| `testFiles`        | \u003ccode\u003estring \u0026#124; string[]\u003c/code\u003e | `['**/*.proof.{ts,tsx}', 'proof/**/*.{ts,tsx}']` | Glob pattern(s) for test files                                                      |\n| `check`            | `boolean`                           | `false`                                          | Enable type checking on test files                                                  |\n| `checkFiles`       | \u003ccode\u003estring \u0026#124; string[]\u003c/code\u003e | Same as `testFiles`                              | Glob pattern(s) for files to check. This option is only used when `check` is `true` |\n| `compilerOptions`  | `ts.CompilerOptions`                | `{}`                                             | Additional TypeScript compiler options to override those in tsconfig.json           |\n| `plugins`          | `Plugin[]`                          | `[]`                                             | Typroof plugins that extend functionality                                           |\n\n### Import Notice\n\nWhen creating your configuration file, import from the specific subpath:\n\n```typescript\n// ✅ Correct import\nimport { defineConfig } from 'typroof/config';\n```\n\n### CLI Usage\n\nYou can run Typroof from the command line:\n\n```shell\n# Run in current directory\nnpx typroof\n\n# Run in specific directory\nnpx typroof /path/to/project\n\n# Specify test files\nnpx typroof --files \"src/**/*.proof.ts\"\n\n# Use custom config file\nnpx typroof --config ./typroof.config.ts\n\n# Use custom tsconfig.json and enable type checking\nnpx typroof --check --project ./tsconfig.test.json\n\n# Get help\nnpx typroof --help\n```\n\nAvailable options:\n\n- `--files, -f`: Glob pattern(s) for test files.\n- `--config, -c`: Path to config file.\n- `--project, -p`: Path to tsconfig.json file.\n- `--check`: Enable type checking on test files.\n- `--check-files`: Glob pattern(s) for files to check. This option is only used when `--check` is enabled.\n- `--help`: Show help information.\n- `--version`: Show version information.\n\n## Programmatic Usage\n\nYou can use Typroof programmatically within your own Node.js scripts or tools:\n\n```typescript\nimport typroof, { formatGroupResult, formatSummary } from 'typroof';\n\n// Run Typroof with default options\nconst startedAt = new Date();\nconst results = await typroof();\nconst finishedAt = new Date();\n\n// Display results\nfor (const result of results) {\n  console.log(formatGroupResult(result.rootGroupResult));\n  console.log();\n}\n\n// Print summary\nconsole.log(\n  formatSummary({ groups: results.map((r) =\u003e r.rootGroupResult), startedAt, finishedAt }),\n);\n```\n\n### API Options\n\nThe `typroof()` function accepts the same options available in the configuration file, plus an additional `cwd` option:\n\n```typescript\n// Run with custom options\nconst results = await typroof({\n  // Standard config options\n  testFiles: ['src/**/*.proof.ts'],\n  compilerOptions: { strictNullChecks: true },\n  plugins: [],\n  // Additional API-only option\n  cwd: '/path/to/project', // Custom working directory\n});\n```\n\nThe `cwd` option sets the base directory for:\n\n- Finding the default `tsconfig.json` file (`${cwd}/tsconfig.json`).\n- Resolving relative paths in your configuration.\n\nIf not provided, `cwd` defaults to `process.cwd()`.\n\n## Plugin API \u0026 How It Works\n\nTyproof supports plugins to extend its functionality with custom matchers. The plugin system allows you to:\n\n- Create custom type matchers.\n- Share matchers as reusable packages.\n- Extend Typroof’s core functionality.\n\n### Creating a Custom Matcher\n\nA matcher in Typroof consists of two parts:\n\n1. **Type validator**: A type-level function that checks relationships at compile time.\n2. **Analyzer**: A runtime function that further analyzes types using the TypeScript compiler API, and reports errors in CLI.\n\nNote that many built-in matchers in Typroof are type-level only matchers whose analyzers are only used to report errors, e.g., `equal`, `extend`, `beNever`, etc. While some matchers require runtime analysis in its analyzer, e.g., `error`, which checks if a type emits an error with TypeScript compiler API.\n\nHere’s a simple custom matcher example (a type-level only matcher):\n\n```typescript\nimport { match } from 'typroof/plugin';\nimport type { Actual, Expected, Validator } from 'typroof/plugin';\n\n// 1. Create and export the matcher\nexport const startsWith = \u003cU extends string\u003e(prefix?: U) =\u003e match\u003c'startsWith', U\u003e();\n\n// 2. Define the validator type\ndeclare module 'typroof/plugin' {\n  interface ValidatorRegistry {\n    startsWith: StartsWithValidator;\n  }\n}\ntype Cast\u003cT, U\u003e = T extends U ? T : U;\n// Use a type-level function (i.e. HKT) to define a type-level validator\ninterface StartsWithValidator extends Validator {\n  // Return `true` or `false` to indicate whether the assertion passed or not\n  return: Actual\u003cthis\u003e extends `${Cast\u003cExpected\u003cthis\u003e, string\u003e}${string}` ? true : false;\n}\n\n// 3. Create a plugin to register the analyzer\nimport type { Plugin } from 'typroof/plugin';\n\nexport const startsWith = (): Plugin =\u003e ({\n  name: 'typroof-plugin-starts-with',\n  analyzers: {\n    // `actual` and `expected` are the types passed to the matcher (T and U).\n    startsWith: (actual, expected, { not, typeChecker }) =\u003e {\n      // NOTE: This analyzer is only called when the type-level validation fails\n      // We use TypeScript compiler API to get the text of the type:\n      const actualType = typeChecker.typeToString(actual.type);\n      const expectedType = typeChecker.typeToString(expected);\n      // Throw a string to report the error\n      throw `Expected ${actual.text} ${not ? 'not ' : ''}to start with \"${expectedType}\", but got \"${actualType}\"`;\n    },\n  },\n});\n\n// 4. Use the plugin in your config\n// typroof.config.ts\nimport { defineConfig } from 'typroof/config';\n\nexport default defineConfig({\n  plugins: [startsWith()],\n});\n```\n\n`Validator`s in Typroof are type-level functions (HKTs) compatible with the [hkt-core](https://github.com/Snowflyt/hkt-core) V1 standard, see [its documentation](https://github.com/Snowflyt/hkt-core) for more information.\n\n### Deep Dive: Custom Error Messages for Validators\n\nIn the previous example, Typroof already automatically generates a compile-time error message if the assertion fails:\n\n```typescript\nexpect\u003c'foobar'\u003e().to(startsWith\u003c'bar'\u003e);\n//                    ~~~~~~~~~~~~~~~~~\n//    Argument of type '...' is not assignable to parameter\n//   of type \"Validation failed: startsWith\u003c'foobar', 'bar'\u003e\"\n```\n\nHowever, it’s not very readable, compared to Typroof’s built-in matchers:\n\n```typescript\nexpect\u003cAppend\u003c'foo', 'bar'\u003e\u003e().to(equal\u003c'foo'\u003e);\n//                                ~~~~~~~~~~~~\n//            Argument of type '...' is not assignable to parameter\n//          of type \"Expect `'foobar'` to equal `'foo'`, but does not\"\n```\n\nThe magic behind this is that the `equal` matcher’s validator returns a string type instead of a boolean type if the assertion fails, which is used as the error message.\n\nLet’s rewrite the `startsWith` matcher to return a string type as the error message:\n\n```typescript\nimport type { Actual, Expected, IsNegated, Stringify, Validator } from 'typroof/plugin';\n\ndeclare module 'typroof/plugin' {\n  interface ValidatorRegistry {\n    startsWith: StartsWithValidator;\n  }\n}\n\ninterface StartsWithValidator extends Validator {\n  // `IsNegated\u003cthis\u003e` is `true` if `.not` is used, otherwise `false`.\n  return: IsNegated\u003cthis\u003e extends false ?\n    // If `.not` is not used\n    Actual\u003cthis\u003e extends `${Cast\u003cExpected\u003cthis\u003e, string\u003e}${string}` ?\n      true\n    : `Expect \\`${Stringify\u003cActual\u003cthis\u003e\u003e}\\` to start with \\`${Stringify\u003cExpected\u003cthis\u003e\u003e}\\`, but does not`\n  : // If `.not` is used\n  Actual\u003cthis\u003e extends `${Cast\u003cExpected\u003cthis\u003e, string\u003e}${string}` ? false\n  : `Expect the type not to start with \\`${Stringify\u003cExpected\u003cthis\u003e\u003e}\\`, but does`;\n}\n```\n\nThe exported `Stringify` utility type is used to convert a type to a literal string type, which is used as the error message. The implementation of `Stringify` is quite complex, and it is recommended to use it instead of implementing your own.\n\n\u003e [!TIP]\n\u003e\n\u003e `Stringify` supports custom serializers. Say you have a custom type `interface Response\u003cT\u003e { code: number; data: T }`. Instead of receiving `\"{ code: number; data: string }\"` as the result of `Stringify\u003cResponse\u003cstring\u003e\u003e`, you might prefer the more concise `\"Response\u003cstring\u003e\"`. You can achieve this by adding a custom serializer to `Stringify` via the [module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) syntax:\n\u003e\n\u003e ```typescript\n\u003e import type { Serializer, Stringify, Type } from 'typroof/plugin';\n\u003e\n\u003e declare module 'typroof/plugin' {\n\u003e   interface StringifySerializerRegistry {\n\u003e     Response: { if: ['extends', Response\u003cunknown\u003e]; serializer: ResponseSerializer };\n\u003e   }\n\u003e }\n\u003e interface ResponseSerializer extends Serializer\u003cResponse\u003cunknown\u003e\u003e {\n\u003e   return: `Response\u003c${Type\u003cthis\u003e['data']}\u003e`;\n\u003e }\n\u003e\n\u003e type TestResult = Stringify\u003cResponse\u003cstring\u003e\u003e;\n\u003e //   ^?: \"Response\u003cstring\u003e\"\n\u003e ```\n\u003e\n\u003e Similar to `Validator`s, `Serializer`s are also type-level function but return a string type. The `Type\u003cthis\u003e` utility type is used to get the type passed to the current serializer. Except for the `serializer` property, you also have to add a `if` property as a predicate to determine whether the serializer should be used. Valid forms of the `if` property are:\n\u003e\n\u003e - `['extends', T]`: The type must extend `T`.\n\u003e - `['equals', T]`: The type must be exactly equal to `T`.\n\u003e - A custom type-level function (HKT) that returns a boolean type. See the documentation of [hkt-core](https://github.com/Snowflyt/hkt-core) for more information.\n\u003e\n\u003e Custom serializers also boost `Stringify` utility’s speed in computing results, which can prevent Typroof from slowing down or crashing when handling complex types.\n\n### Deep Dive: Advanced Example - `error` Matcher\n\nUp to now, we have seen how to create a type-level only matcher. But what if we want to create a matcher that requires runtime analysis?\n\n`error` matcher checks if a type emits an error with TypeScript compiler API. It is a good example to show how to create a matcher that requires runtime analysis.\n\nLet’s take a look at how `error` is implemented:\n\n```typescript\nimport { match } from 'typroof/plugin';\n\nimport type { ToAnalyze } from 'typroof/plugin';\n\nexport const error = match\u003c'error'\u003e();\n\ndeclare module 'typroof/plugin' {\n  interface ValidatorRegistry {\n    error: ErrorValidator;\n  }\n}\ninterface ErrorValidator {\n  return: ToAnalyze\u003cnever\u003e;\n}\n\nexport const errorPlugin = (): Plugin =\u003e ({\n  name: 'typroof-plugin-error',\n  analyzers: {\n    error: (actual, _expected, { diagnostics, not, sourceFile, statement }) =\u003e {\n      // Check if a diagnostic error exists for this node\n      const diagnostic = diagnostics.find((diagnostic) =\u003e {\n        const start = diagnostic.start;\n        if (start === undefined) return false;\n\n        const length = diagnostic.length;\n        if (length === undefined) return false;\n\n        const end = start + length;\n        const nodeStart = actual.node.getStart(sourceFile);\n        const nodeEnd = actual.node.getEnd();\n\n        return start \u003e= nodeStart \u0026\u0026 end \u003c= nodeEnd;\n      });\n\n      // Find @ts-expect-error comments that apply to this expression\n      const findTSExpectError = () =\u003e {\n        const sourceText = sourceFile.text;\n\n        // 1. Check for leading comments directly before the statement\n        const leadingComments =\n          ts.getLeadingCommentRanges(sourceText, statement.getFullStart()) || [];\n\n        // 2. Find any internal comments within the statement’s full text range\n        // This helps with multi-line expressions that have inline comments\n        const statementStart = statement.getFullStart();\n        const statementEnd = statement.getEnd();\n        const statementText = sourceText.substring(statementStart, statementEnd);\n\n        // Track all potential comment positions\n        const commentPositions: { start: number; end: number }[] = [\n          ...leadingComments.map((c) =\u003e ({ start: c.pos, end: c.end })),\n        ];\n\n        // Scan the statement for possible comment starts\n        let pos = 0;\n        while (pos \u003c statementText.length) {\n          // Look for // comments\n          if (statementText.substring(pos, pos + 2) === '//') {\n            const startPos = statementStart + pos;\n            let endPos = statementText.indexOf('\\n', pos);\n            if (endPos === -1) endPos = statementText.length;\n            commentPositions.push({\n              start: startPos,\n              end: statementStart + endPos,\n            });\n            pos = endPos + 1;\n            continue;\n          }\n\n          // Look for /* */ comments\n          if (statementText.substring(pos, pos + 2) === '/*') {\n            const startPos = statementStart + pos;\n            const endPos = statementText.indexOf('*/', pos);\n            if (endPos !== -1) {\n              commentPositions.push({\n                start: startPos,\n                end: statementStart + endPos + 2,\n              });\n              pos = endPos + 2;\n              continue;\n            }\n          }\n\n          pos++;\n        }\n\n        // Check all comment positions for @ts-expect-error\n        for (const { end, start } of commentPositions) {\n          const commentText = sourceText.substring(start, end);\n          if (commentText.includes('@ts-expect-error')) {\n            // Ensure this @ts-expect-error is not already used by checking\n            // if there’s a diagnostic that starts at this exact position\n            const isUnused = !diagnostics.some(\n              (d) =\u003e d.start === start \u0026\u0026 d.code === 2578, // TypeScript’s code for @ts-expect-error\n            );\n            if (isUnused) return true;\n          }\n        }\n\n        return false;\n      };\n\n      // Check if error is triggered either by diagnostic or @ts-expect-error\n      const triggeredError = !!diagnostic || findTSExpectError();\n\n      if (not ? triggeredError : !triggeredError) {\n        const actualText = bold(actual.text);\n        throw (\n          `Expect ${actualText} ${not ? 'not ' : ''}to trigger error, ` +\n          `but ${not ? 'did' : 'did not'}.`\n        );\n      }\n    },\n  },\n});\n```\n\nThe `error` matcher returns `ToAnalyze\u003cnever\u003e` as the return type of its validator, which means it requires runtime analysis. In the `error` example, the type-level validation step is omitted, so we simply pass `ToAnalyze\u003cnever\u003e`. However, if you want to create a matcher that requires both type-level validation and runtime analysis, you can return `ToAnalyze\u003cValidatorReturnType\u003e` as the return type of your validator—the validation result type can be accessed via `validationResult` in the 3rd argument of the analyzer.\n\nYou can still return booleans or strings as the return type of your validator in combination with `ToAnalyze\u003cValidatorReturnType\u003e`, where the boolean or string indicates an early exit of the type-level validation step, and `validationResult` will be `undefined` in the analyzer if `false` or string is returned from the validator.\n\n### Publishing a Plugin\n\nIf you want to publish your plugin as a library, it is recommended to export the factory function to create the plugin object as the default export, and export the matchers as named exports:\n\n```typescript\n// In your `index.ts`\nimport { match } from 'typroof/plugin';\n\nimport type { Plugin } from 'typroof/plugin';\n\ndeclare module 'typroof/plugin' {\n  interface ValidatorRegistry {\n    beFoo: /* ... */;\n  }\n}\n\nconst foo = (): Plugin =\u003e ({\n  /* ... */\n});\nexport default foo;\n\n/**\n * [Matcher] Expect the type to be `\"foo\"`.\n */\nexport const beFoo = match\u003c'beFoo'\u003e();\n```\n\nThen your users can use your plugin like this:\n\n```typescript\n// In their `typroof.config.ts`\nimport foo from 'typroof-plugin-example';\n\nexport default defineConfig({\n  plugins: [foo()],\n});\n\n// And somewhere in their test files\nimport { beFoo } from 'typroof-plugin-example';\n\ntest('foo', () =\u003e {\n  expect\u003c'foo'\u003e().to(beFoo);\n});\n```\n\nYou can try a live demo of creating a plugin [here](https://githubbox.com/Snowflyt/typroof/tree/main/examples/simple-plugin).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsnowflyt%2Ftyproof","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsnowflyt%2Ftyproof","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsnowflyt%2Ftyproof/lists"}