Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions packages/type-testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,117 @@ expect<string | null>().toBeNullable() // passes
expect<string | undefined>().toBeOptional() // passes
```

## Vitest Integration

The library provides a native Vitest integration with custom matchers for a more familiar testing experience.

### Installation

```bash
npm install @deessejs/type-testing
```

### Quick Start

```typescript
import { expectType } from '@deessejs/type-testing/vitest'

// Type equality
expectType<string>().toBeType<string>()
expectType<string>().toNotBeType<number>()

// Type extends
expectType<string>().toExtend<string>()
expectType<string>().toNotExtend<number>()

// Property check
expectType<{ a: string }>().toHaveProperty('a')

// Special types
expectType<any>().toBeAny()
expectType<never>().toBeNever()
expectType<unknown>().toBeUnknown()
expectType<void>().toBeVoid()
expectType<undefined>().toBeUndefined()
expectType<null>().toBeNull()

// Nullable/Optional
expectType<string | null>().toBeNullable()
expectType<{ a?: string }>().toBeOptional()

// Structure
expectType<string | number>().toBeUnion()
expectType<[string, number]>().toBeTuple()
expectType<string[]>().toBeArray()

// Inhabitation
expectType<string>().toBeInhabited()
expectType<never>().toBeUninhabited()
```

### Using with Vitest's expect.extend

You can also extend Vitest's expect with the matchers:

```typescript
import { expect, test } from 'vitest'
import { toBeType, toHaveProperty } from '@deessejs/type-testing/vitest'

expect.extend({ toBeType, toHaveProperty })

test('type checks', () => {
expect<string>().toBeType<string>()
expect<{ a: string }>().toHaveProperty('a')
})
```

### Setup File

For automatic matcher registration, add the setup file to your Vitest config:

```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
setupFiles: ['@deessejs/type-testing/vitest/setup']
}
})
```

Or import it in your setup file:

```typescript
// setup.ts
import '@deessejs/type-testing/vitest/setup'
```

### Available Matchers

| Matcher | Description |
|---------|-------------|
| `toBeType<T>()` | Asserts type equality |
| `toNotBeType<T>()` | Asserts type inequality |
| `toExtend<T>()` | Asserts type extends another |
| `toNotExtend<T>()` | Asserts type does not extend another |
| `toHaveProperty<K>()` | Asserts property exists |
| `toBeAny()` | Asserts type is `any` |
| `toBeNever()` | Asserts type is `never` |
| `toBeUnknown()` | Asserts type is `unknown` |
| `toBeVoid()` | Asserts type is `void` |
| `toBeUndefined()` | Asserts type is `undefined` |
| `toBeNull()` | Asserts type is `null` |
| `toBeNullable()` | Asserts type is nullable |
| `toBeOptional()` | Asserts type is optional |
| `toBeUnion()` | Asserts type is a union |
| `toBeTuple()` | Asserts type is a tuple |
| `toBeArray()` | Asserts type is an array |
| `toBeInhabited()` | Asserts type is inhabited |
| `toBeUninhabited()` | Asserts type is uninhabited |

> **Note**: The `toNotBeType` matcher follows a different pattern than standard Vitest (`not.toBeType`). This is intentional as it provides better TypeScript inference. Use `toNotBeType` instead of `.not.toBeType`.

## Compile-time Assertions

### ExpectTrue & ExpectEqual
Expand Down
8 changes: 8 additions & 0 deletions packages/type-testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./vitest": {
"import": "./dist/vitest/index.js",
"types": "./dist/vitest/index.d.ts"
},
"./vitest/setup": {
"import": "./dist/vitest/setup.js",
"types": "./dist/vitest/setup.d.ts"
}
},
"scripts": {
Expand Down
27 changes: 27 additions & 0 deletions packages/type-testing/src/vitest/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider documenting this new Vitest integration in the README so users know how to use it.

* Vitest integration for type-testing.
*
* @packageDocumentation
*/

export {
toBeType,
toNotBeType,
toExtend,
toNotExtend,
toHaveProperty,
toBeAny,
toBeNever,
toBeUnknown,
toBeVoid,
toBeUndefined,
toBeNull,
toBeNullable,
toBeOptional,
toBeUnion,
toBeTuple,
toBeArray,
toBeInhabited,
toBeUninhabited,
expectType
} from './matchers.js'
183 changes: 183 additions & 0 deletions packages/type-testing/src/vitest/matchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* Custom Vitest matchers for type testing.
*
* These matchers provide compile-time type checking. At runtime, they return
* undefined to allow the test to pass. The actual type checking happens
* at compile time through TypeScript's type system.
*/

import type { Equal, NotEqual } from '../types/equality.js'
import type { IsAny, IsNever, IsUnknown, IsVoid, IsUndefined, IsNull, IsNullable, IsOptional } from '../types/special.js'
import type { IsUnion, IsTuple, IsArray } from '../types/union.js'
import type { IsInhabited, IsUninhabited } from '../types/inhabitation.js'
import type { HasProperty } from '../types/property.js'
import type { CheckPass } from '../api/check.js'

/**
* Creates a Vitest matcher for type equality.
*
* @example
* ```typescript
* import { expect, test } from 'vitest'
* import { toBeType } from '@deessejs/type-testing/vitest'
*
* expect.extend({ toBeType })
*
* test('type check', () => {
* expectType<string>().toBeType<string>()
* })
* ```
*/
export function toBeType<T, U>(): Equal<T, U> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for type inequality.
*/
export function toNotBeType<T, U>(): NotEqual<T, U> extends true ? CheckPass : never {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matcher follows a different naming convention than standard Jest/Vitest matchers which typically use toBe/not.toBe pattern. Consider adding toBeNotType as an alias or documenting this intentional deviation.

return undefined as any
}

/**
* Creates a Vitest matcher for type extends check.
*/
export function toExtend<T, U>(): T extends U ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for type not extends check.
*/
export function toNotExtend<T, U>(): T extends U ? never : CheckPass {
return undefined as any
}

/**
* Creates a Vitest matcher for property existence.
*/
export function toHaveProperty<T, K extends keyof T>(): HasProperty<T, K> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for any type check.
*/
export function toBeAny<T>(): IsAny<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for never type check.
*/
export function toBeNever<T>(): IsNever<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for unknown type check.
*/
export function toBeUnknown<T>(): IsUnknown<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for void type check.
*/
export function toBeVoid<T>(): IsVoid<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for undefined type check.
*/
export function toBeUndefined<T>(): IsUndefined<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for null type check.
*/
export function toBeNull<T>(): IsNull<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for nullable type check.
*/
export function toBeNullable<T>(): IsNullable<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for optional type check.
*/
export function toBeOptional<T>(): IsOptional<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for union type check.
*/
export function toBeUnion<T>(): IsUnion<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for tuple type check.
*/
export function toBeTuple<T>(): IsTuple<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for array type check.
*/
export function toBeArray<T>(): IsArray<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for inhabited type check.
*/
export function toBeInhabited<T>(): IsInhabited<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Creates a Vitest matcher for uninhabited type check.
*/
export function toBeUninhabited<T>(): IsUninhabited<T> extends true ? CheckPass : never {
return undefined as any
}

/**
* Helper function to create a type holder for testing.
*
* @example
* ```typescript
* expectType<string>().toBeType<string>()
* ```
*/
export function expectType<T>(): {
toBeType<U>(): Equal<T, U> extends true ? CheckPass : never
toNotBeType<U>(): NotEqual<T, U> extends true ? CheckPass : never
toExtend<U>(): T extends U ? CheckPass : never
toNotExtend<U>(): T extends U ? never : CheckPass
toHaveProperty<K extends keyof T>(): HasProperty<T, K> extends true ? CheckPass : never
toBeAny(): IsAny<T> extends true ? CheckPass : never
toBeNever(): IsNever<T> extends true ? CheckPass : never
toBeUnknown(): IsUnknown<T> extends true ? CheckPass : never
toBeVoid(): IsVoid<T> extends true ? CheckPass : never
toBeUndefined(): IsUndefined<T> extends true ? CheckPass : never
toBeNull(): IsNull<T> extends true ? CheckPass : never
toBeNullable(): IsNullable<T> extends true ? CheckPass : never
toBeOptional(): IsOptional<T> extends true ? CheckPass : never
toBeUnion(): IsUnion<T> extends true ? CheckPass : never
toBeTuple(): IsTuple<T> extends true ? CheckPass : never
toBeArray(): IsArray<T> extends true ? CheckPass : never
toBeInhabited(): IsInhabited<T> extends true ? CheckPass : never
toBeUninhabited(): IsUninhabited<T> extends true ? CheckPass : never
} {
return {} as any

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good use of the type holder pattern. The return type casting with {} as any is necessary but consider adding a comment explaining why this is needed for the type testing pattern.

}
19 changes: 19 additions & 0 deletions packages/type-testing/src/vitest/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Vitest setup for type-testing.
*
* This module provides integration with Vitest.
*
* @example
* ```typescript
* // vitest.config.ts
* import { defineConfig } from 'vitest/config'
*
* export default defineConfig({
* test: {
* setupFiles: ['@deessejs/type-testing/vitest/setup']
* }
* })
* ```
*/

export { expectType } from './matchers.js'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setup file exports expectType but doesn't actually extend Vitest's expect with the matchers. The README suggests using this as a setup file (line 414), but it only re-exports expectType without calling expect.extend(). This means users need to manually call expect.extend({ toBeType, ... }) or import the matchers directly.

If the setup file is meant to auto-register matchers, it should include:

import { expect } from 'vitest'
import * as matchers from './matchers.js'

expect.extend(matchers)

Otherwise, consider clarifying in the documentation that this only exports expectType for convenience.

17 changes: 17 additions & 0 deletions packages/type-testing/tests/vitest-setup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Vitest setup module tests.
*/

import { describe, it, expect } from 'vitest'

// Import from the setup module
import '../src/vitest/setup'

describe('Vitest setup', () => {
it('should export expectType from setup', async () => {
const { expectType } = await import('../src/vitest/setup')
expect(expectType).toBeDefined()
const result = expectType<string>()
expect(result).toBeDefined()
})
})
Loading
Loading